Compare commits

...
338 Commits
Author SHA1 Message Date
Joe Nelson 593f247abb Expose more heroku config vars 2015-09-01 23:16:30 -07:00
Joe Nelson add63ac25b Keep the dilapidated release script alive 2015-09-01 23:13:47 -07:00
Joe Nelson 1ef8cc5048 Bump patch version 2015-09-01 22:29:13 -07:00
Joe Nelson c5836e0c9e Merge pull request #275 from diogob/fix_all_media_types_in_accept
Fix */* in accept headers
2015-09-01 13:10:02 -07:00
Diogo Biazus 922aa702a2 Adds fix to changelog 2015-09-01 15:58:07 -04:00
Diogo Biazus 89c581816e Adds */* as a valid media type that will return json [fix #274] 2015-09-01 15:57:59 -04:00
Joe Nelson 3d670b9c03 bump minor version 2015-08-28 19:19:37 -07:00
Joe Nelson 5bf644867f Merge pull request #271 from begriffs/surprise-404
Allow continued auth access after db errors
2015-08-26 20:58:55 -07:00
Joe Nelson fcdae73f49 Note fix in changelog 2015-08-26 20:49:23 -07:00
Joe Nelson 1656fb9f57 Let the transaction reset the role and user id for us 2015-08-26 20:49:23 -07:00
Joe Nelson 594327924c Set role locally in a tx to ensure it is reset after error 2015-08-26 20:49:23 -07:00
Joe Nelson 480800edbd Problem after exceptions when authed
Reproduces #264
2015-08-26 20:49:23 -07:00
Joe Nelson 9b01d1b1ab Helpful directions in contributing doc 2015-08-26 20:47:22 -07:00
Joe Nelson ed810bc380 Operator negation 2015-08-21 19:35:48 -07:00
Joe Nelson 789db9a017 Merge pull request #266 from diogob/adds_not_unary_operator
Adds not as a keyword that can optionally be prepended to any operator in a parameter value [fix #173]
2015-08-21 19:25:02 -07:00
Diogo Biazus 52626cc86d Adds test cases for not operator in equality, inequality, like, ilike, tesarch (@@) and is null queries 2015-08-21 15:08:52 -04:00
Diogo Biazus 32b97ef076 Adds not as a keyword that can optionally be prepended to any operator in a parameter value 2015-08-21 10:33:41 -04:00
Joe Nelson 8e319321c9 RPC and Stack 2015-08-20 22:37:19 -07:00
Joe Nelson cdac6d385c Merge pull request #228 from begriffs/rpc
Expose stored procedures
2015-08-20 22:27:30 -07:00
Joe Nelson d65d011e5e Tests for rpc 2015-08-20 22:14:09 -07:00
Joe Nelson 28f771324d Nest response JSON more shallowly 2015-08-20 22:14:08 -07:00
Joe Nelson cf04fbd6ea Call procedures that return setof, not just text
The output is too deeply nested however
2015-08-20 21:52:46 -07:00
Joe Nelson bb511f0df2 WIP: call stored pprocedures that emit plain text
The beginning of #114
2015-08-20 21:52:46 -07:00
Joe Nelson f564fb0977 Rename QualifiedTable to encompass proc names as well 2015-08-20 21:35:12 -07:00
Diogo BiazusandJoe Nelson 3b017dfdf6 Adds 415 response for any non-empty Accept header different from application/json or text/csv. Uses apropriate Content-Type header when sending CSV format. 2015-08-20 20:47:36 -07:00
Diogo BiazusandJoe Nelson cb7d00b839 Adds tags file to gitignore 2015-08-20 20:47:36 -07:00
Joe Nelson 010e18ea0b Merge pull request #269 from diogob/build_with_stack
Build with stack
2015-08-20 15:29:10 -07:00
Diogo Biazus c34f96ce2e Removes body matcher to compile and test against any aeson version >= 0.8 2015-08-20 16:43:57 -04:00
Diogo Biazus f9d50018d9 Rollback to aeson 0.8.0.2 to allow building with stackage, and adds stack.yml 2015-08-20 15:50:28 -04:00
Diogo Biazus 6c1233fcec Adss stack-work directory to gitignore 2015-08-20 15:15:53 -04:00
Joe Nelson acd8e92d24 Note the NOT IN addition 2015-08-17 09:40:49 -07:00
Joe Nelson 559d370a89 Merge pull request #263 from rall/notin
NOT IN queries
2015-08-17 09:38:45 -07:00
Richard Allaway 4cf51b7005 fixes spec for changed error message from updated aeson library 2015-08-17 11:10:15 -04:00
Richard Allaway 4e77492797 adds a spec for NOT IN query case 2015-08-17 10:24:51 -04:00
Richard Allaway f16e2e3ee5 adds 'not in' query 2015-08-17 10:24:51 -04:00
Joe Nelson ebc8c387e0 Thanks @diogob! 2015-08-15 12:14:20 -07:00
Joe Nelson 734484714c CSV responses! 2015-08-15 11:59:12 -07:00
Joe Nelson adac39bd7c Use Content-Type text/csv for CSV responses 2015-08-15 11:57:57 -07:00
Diogo Biazus 9d5011e864 Implements CSV resnponse for the appropriate accept headers 2015-08-14 11:04:26 -04:00
Diogo Biazus ca40ba1fda Refactors app function to DRY header lookups 2015-08-14 11:04:26 -04:00
Joe Nelson 894455f2cd Relax hasql deps for packdeps checker 2015-08-12 00:05:38 -07:00
Joe Nelson 6cb73062a9 Better shields 2015-08-11 23:18:47 -07:00
Joe Nelson 86c68d191c Updated maintenance note in contributing doc 2015-08-09 14:04:57 -07:00
Joe Nelson d20c252cb3 Adjust version of hasql-postgres for packdeps 2015-08-08 12:57:44 -07:00
Joe Nelson cd6b688f7f Merge pull request #244 from diogob/adds_materialized_views_to_root
Adds materialized views to list of relations in GET / [#242]
2015-08-01 16:16:23 -07:00
Joe Nelson 49f41d8edb Merge pull request #243 from diogob/fix_count_column_name_case
Fixes error code 42803 when trying to query a view with a column named count
2015-08-01 16:15:02 -07:00
Diogo Biazus b45953dff8 Mentions fix in CHANGELOG 2015-08-01 01:23:31 -04:00
Diogo Biazus 8075d7e51a Mentions fix in CHANGELOG 2015-08-01 01:21:42 -04:00
Diogo Biazus ab0170ffaf Adds materialized views to list of relations in GET / [#242] 2015-08-01 01:09:12 -04:00
Diogo Biazus 449cacdacf Fixes error code 42803 when trying to query a view with a column named count. 2015-08-01 00:15:17 -04:00
Joe Nelson e724c2df00 Merge pull request #230 from edelans/master
Add link to Jonathan Harrington's nice tutorial
2015-07-26 10:17:39 -07:00
Joe Nelson 12dc180065 Merge pull request #237 from datasaur/master
Enable log capture if stdout is not a terminal (issue #229)
2015-07-24 09:51:37 -07:00
MattK 8eae978eae Enable log capture if stdout is not a terminal 2015-07-24 12:14:18 -04:00
Edouard de Lansalut 019d53bca1 Add link to Jonathan Harrington's nice tutorial 2015-07-22 09:47:14 +02:00
Joe Nelson e1d7dc3dea Note computed columns in changelog 2015-07-21 22:34:45 -07:00
Joe Nelson 28a2826fa8 Merge pull request #221 from diogob/allow_virtual_fields_in_where
Qualifies columns of WHERE clauses so we can use computed columns as filters
2015-07-21 22:31:10 -07:00
Diogo Biazus c76864a653 Qualifies columns used in WHERE clauses so we can use computed columns as filters 2015-07-10 12:35:24 -04:00
Joe Nelson c0d44232a5 Note Debian changes in changelog 2015-07-09 22:52:14 -07:00
Joe Nelson e34e92eb44 Merge pull request #216 from mkhon/master
Debian init script for postgrest.
2015-07-09 22:49:11 -07:00
Joe Nelson 956f73d997 Regression test for situation reported in issue #203 2015-07-03 22:51:04 -07:00
Joe Nelson df04d26c15 Merge pull request #219 from diogob/check_postgresql_version
Verifies PostgreSQL version is supported (+9.2) before spawning server
2015-07-02 13:21:29 -07:00
Diogo Biazus 9c69553373 Verifies PostgreSQL version is supported (+9.2) before spawning server [fixes #157] 2015-07-02 09:26:58 -04:00
Joe Nelson 6604293ac1 Remove regex-tdfa-text to allow building in GHC 7.10
Fixes #212
2015-07-01 21:55:35 -07:00
Max Khon 60a61adbce Use POSTGREST_USER. 2015-06-24 18:30:56 +06:00
Max Khon b56ab47f84 Debian init script for postgrest. 2015-06-24 18:07:30 +06:00
Joe Nelson 25492a089b Merge pull request #214 from diogob/refactor_to_bool
Removes toBool function as we now cast the 'YES/NO' values to boolean in PostgreSQL's queries
2015-06-22 22:29:07 -07:00
Diogo Biazus e87be593c0 Removes toBool function as we now cast the 'YES/NO' values to boolean in PostgreSQL's queries 2015-06-22 18:09:45 -04:00
Joe Nelson 1937363fc8 Note @diogob's contribution 2015-06-21 22:18:51 -07:00
Joe Nelson d68cbec25c Merge pull request #209 from diogob/insertable_views_with_triggers
Changes the insertable to true in views that are insertable through triggers [fixes #206]
2015-06-21 22:16:43 -07:00
Diogo Biazus f4011e5d8c Changes the insertable to true in views that are insertable through triggers [fixes #206] 2015-06-22 00:16:23 -04:00
Joe Nelson e93c96a6f8 Link to Heroku troubleshooting Wiki in readme 2015-06-21 14:01:42 -07:00
Joe Nelson 070f67e9c6 Merge pull request #213 from begriffs/packdeps
Loosen dependency version constraints for packdeps
2015-06-20 12:16:05 -07:00
Joe Nelson 61dac3b02b Loosen dependency version constraints for packdeps 2015-06-20 12:12:36 -07:00
Joe Nelson 169157ec6d Thanks @framp 2015-06-17 20:24:10 -07:00
Joe Nelson bd304c9fc3 Bump version 2015-06-03 21:22:17 -07:00
Joe Nelson 3989aaa144 Note @framp's auth id change 2015-05-26 14:58:59 -07:00
Joe Nelson 1d51a5f543 Merge pull request #201 from framp/master
User_id support (via user_vars)
2015-05-26 14:55:56 -07:00
Joe Nelson e4dafad64d Use github release feature to host binaries 2015-05-25 21:56:47 -07:00
Joe Nelson 3f31c60f1d Mention JWT in readme 2015-05-25 21:46:05 -07:00
Federico Rampazzo 83f48dcd15 User_id support (via user_vars) 2015-05-24 03:26:42 +01:00
Joe Nelson 1cc53245c5 Full text search in changelog 2015-05-23 08:21:02 -07:00
Joe Nelson f24ba048af Merge pull request #199 from diogob/tsearch_operator
Tsearch operator
2015-05-23 08:15:44 -07:00
Diogo Biazus a980db6d2d Adds spec for @@ operator and implements it in PgQuery 2015-05-22 12:28:26 -04:00
Diogo Biazus 4277284a69 Adds table tsearch to test for @@ operator against a tsvector field. Fixes StructureSpec accordingly. 2015-05-22 12:15:14 -04:00
Joe Nelson b070994912 Patch version bump for conditional -Werror flag 2015-05-21 00:18:26 -07:00
Joe Nelson 988df54e53 Use Werror on CI 2015-05-21 00:18:03 -07:00
Joe Nelson 4bbf053896 Bump version 2015-05-20 09:42:51 -07:00
Joe Nelson da79f1da3e Merge pull request #193 from srid/makelibrary
Make postgrest a library
2015-05-19 23:59:16 -07:00
Sridhar Ratnakumar c3ad87ffaf Make postgrest usable as a library 2015-05-19 23:40:39 -07:00
Joe Nelson 035acebf59 Update CHANGELOG.md 2015-05-19 22:23:49 -07:00
Joe Nelson 0b665676d7 Update CHANGELOG.md 2015-05-19 22:21:28 -07:00
Joe Nelson dcf62b020f Merge pull request #194 from framp/jwt
JWT support
2015-05-19 22:17:34 -07:00
Federico Rampazzo 77aecc9e86 JWT support 2015-05-20 06:01:50 +01:00
Joe Nelson e35ad0f340 Update changelog, remove debugging 2015-05-12 10:55:51 -07:00
Joe Nelson 709e70561f Provide more information in PATCH response
* 404 if no records updated
* Range header for number updated
* Full results depending on Prefer header

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

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

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

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

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

Anyhow, without it order does not work.

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

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

Fixes #92
Fixes #40
2014-12-21 19:34:21 -08:00
Joe Nelson 07b039370d Postgres error message parser 2014-12-17 14:15:12 -08:00
Joe Nelson dbe1f59066 Update project name in strings
Fixes #105
2014-12-17 10:24:24 -08:00
Joe Nelson 5f4212e2fd Link to CI report from badge 2014-12-16 20:48:17 -08:00
Joe Nelson 9712549d64 Use "::unknown" type casts to overcome binary protocol restrictions
Fixes #112
2014-12-16 20:09:01 -08:00
Joe Nelson 19b62c00ee Enclose entire app response in single Tx 2014-12-12 13:55:09 -08:00
Joe Nelson d763e27bb2 More readable conditional 2014-12-07 21:32:36 -08:00
Joe Nelson 4f9132af43 Adjust tests to use new-style app config 2014-12-07 18:54:27 -08:00
Adam C. Baker 638cd3d49d update run example for new CLI 2014-12-07 18:51:51 -08:00
Joe Nelson b302843e10 Parse args more sensibly, and use correct port for postgres connection 2014-12-07 18:42:18 -08:00
Joe Nelson 1fd757c2ad Remove coerce dependency for ghc 7.6 2014-12-07 14:46:33 -08:00
Joe Nelson 782d6520a2 Upgrade hasql to 0.4.0 2014-12-07 00:11:14 -08:00
Joe Nelson 4d8c508a17 Parse db uri again 2014-12-06 23:21:49 -08:00
Joe Nelson beb61d8a05 Pend a test until complication is resolved with #107 2014-12-06 18:46:34 -08:00
Joe Nelson c0e4d64423 Merge branch 'logisch' 2014-12-06 17:43:04 -08:00
Joe Nelson 605372f829 Set permissions correctly for structure spec 2014-12-06 17:42:42 -08:00
Joe Nelson 7d58097449 Be sure to reset db before ALL tests 2014-12-06 17:42:42 -08:00
Joe Nelson a45718e992 Re-enable authentication 2014-12-06 17:42:22 -08:00
Joe Nelson 13bc9a4510 Lock hasql version for now 2014-12-06 17:42:22 -08:00
Joe Nelson fb6e297e88 Do not include .0 on integers in generated links 2014-12-06 17:42:22 -08:00
Joe Nelson 5900475460 Reset db between tests rather than using transactions to accomodate Hasql limitation 2014-12-06 17:42:21 -08:00
Joe Nelson 4eb4d8da1e Removed test for RFC compliance
Who would send Content-Range anyway, that is weird in this context
2014-12-06 17:42:21 -08:00
Joe Nelson f1ecbec543 Do not quote params in Location header
Also treat the enum field in OPTIONS as an array uniformly
2014-12-06 17:42:21 -08:00
Joe Nelson 42faaa97c9 Include all fields in Location header when no primary keys defined 2014-12-06 17:42:21 -08:00
Joe Nelson e3d3a07d14 Fun, fun, offbyone 2014-12-06 17:42:21 -08:00
Joe Nelson ca4138df2a Send back Location header on insert 2014-12-06 17:42:21 -08:00
Joe Nelson 0ebb83de3a Include cors middleware in test 2014-12-06 17:42:21 -08:00
Joe Nelson dc43bcbca9 Silence the database fixture output from stdout 2014-12-06 17:42:21 -08:00
Joe Nelson 8e9bbe6dcb Fix insert 2014-12-06 17:42:20 -08:00
Joe Nelson eb7e9586ab Handle missing table sql errors with 404 2014-12-06 17:42:20 -08:00
Joe Nelson 21b0b02c03 Temporarily use psql to load schemas 2014-12-06 17:42:20 -08:00
Joe Nelson dba17b895e Bye Travis, hi Circle 2014-12-06 17:42:20 -08:00
Joe Nelson d6e3526bff Use Text for textual data
Also upgrade Hasql
2014-12-06 17:42:20 -08:00
Joe Nelson f6b7b42d75 Feature specs compile 2014-12-06 17:42:20 -08:00
Joe Nelson da79f6cea1 Share connection pool with all http clients 2014-12-06 17:42:20 -08:00
Joe Nelson d2888c62f1 hasql wants to rowParse text not bytestring 2014-12-06 17:42:19 -08:00
Joe Nelson dcbecf085f Program compiles but without auth, uri parsing, or json error reporting 2014-12-06 17:42:19 -08:00
Joe Nelson ae9fe506be App.hs typechecks 2014-12-06 17:42:19 -08:00
Joe Nelson 70d9641e35 WIP: More files converted to Hasql 2014-12-06 17:42:19 -08:00
Joe Nelson 8ebfbccd08 WIP: converting to Hasql 2014-12-06 17:42:19 -08:00
Joe Nelson b74b91b1c0 Interpolate json string values in queries without extra quoting 2014-12-06 17:42:19 -08:00
Joe Nelson dd2adf4daa Tests compile, run, and fail
Temporarily disabled unit tests
2014-12-06 17:42:19 -08:00
Joe Nelson d791f3939e The Dbapi module is now App 2014-12-06 17:42:18 -08:00
Joe Nelson d9377fa50a Fix select star and json stuff 2014-12-06 17:42:18 -08:00
Joe Nelson f7e0386d1c Fix empty results 2014-12-06 17:42:18 -08:00
Joe Nelson 1b8f0f2829 Main compiles. Still bugs though 2014-12-06 17:42:18 -08:00
Joe Nelson 0f849e9bf1 Move cors things to Main 2014-12-06 17:42:18 -08:00
Joe Nelson f21559ea81 Clean up App.hs 2014-12-06 17:42:18 -08:00
Joe Nelson 61174cfa17 Add user creation route 2014-12-06 17:42:18 -08:00
Joe Nelson 8c073cedc6 Add PATCH handler 2014-12-06 17:42:17 -08:00
Joe Nelson 361cffc283 Add PUT handler 2014-12-06 17:42:17 -08:00
Joe Nelson 8894400f5c Add POST handler 2014-12-06 17:42:17 -08:00
Joe Nelson b5f054d976 WIP: GET route 2014-12-06 17:42:17 -08:00
Joe Nelson cb80dba234 WIP: converting app request handlers 2014-12-06 17:42:17 -08:00
Joe Nelson 9b7af0296e Fix auth functions 2014-12-06 17:42:17 -08:00
Joe Nelson 9d363da8f9 Consolidate auth functions 2014-12-06 17:42:17 -08:00
Adam C. BakerandJoe Nelson 41a982fb31 Remove HDBC imports 2014-12-06 17:42:17 -08:00
Joe Nelson 7196fbc001 WIP: use postgresql-simple in pgstructure 2014-12-06 17:42:16 -08:00
Joe Nelson 2f91fdd8b5 WIP: more clean query functions 2014-12-06 17:42:16 -08:00
Joe Nelson cb467b62c1 WIP: cleaner PgQuery functions 2014-12-06 17:42:16 -08:00
Joe Nelson 94ab57941d Thank you! 2014-12-05 16:27:00 -08:00
Joe Nelson bed759333d Name changes in readme 2014-12-05 16:05:02 -08:00
Adam C. Baker 2b76c87d1d trivial test change to illustrate alternative to pick 2014-11-11 21:29:40 -08:00
Joe Nelson 7bbeeb7c61 Merge branch 'patch' 2014-11-06 23:53:50 -08:00
Joe Nelson 58adb5e828 Allow patch requests
Fixes #87
2014-11-06 23:49:29 -08:00
Joe Nelson 76ea92bfce Derp, actually put the data on a put request 2014-11-06 18:31:13 -08:00
Joe Nelson ffa2870061 Merge branch 'enforce-new-warp' 2014-11-06 15:46:40 -08:00
Joe Nelson 742ff027ba Require warp >= 3.0.2 for setServerName
Fixes #94
2014-11-06 15:35:17 -08:00
Joe Nelson a29f2dd107 Postgres on travis does not recognize lock_timeout 2014-11-06 14:48:15 -08:00
Joe Nelson 648c770cb4 Merge branch 'selective-transactions' 2014-11-06 14:22:23 -08:00
Joe Nelson f1c374403a Remove noisy notifications 2014-11-06 14:19:19 -08:00
Joe Nelson 5cbbe12b65 Use transactions for only patch and put 2014-11-06 14:19:19 -08:00
Joe Nelson f58e3db924 Disable transactions for read-only requests 2014-11-06 14:19:19 -08:00
Joe Nelson 0e57bee90d Merge pull request #93 from begriffs/repackage
declare required extensions
2014-11-06 13:56:47 -08:00
Adam C. Baker 74855db87b declare required extensions
and remove default extension pragmas (OverloadedStrings)
2014-11-05 00:32:10 -08:00
Joe Nelson 8c6b34aaf2 Merge branch 'native-sql-format' 2014-11-03 23:49:21 -08:00
Joe Nelson cc2c65a9ab Build escaped queries in the app itself
universally replace String with Text
2014-11-03 23:30:04 -08:00
Joe Nelson c27f178754 Create functions that mimic Postgres' format() command 2014-11-03 19:53:54 -08:00
Joe Nelson 36f7ac3ff4 Merge branch 'speed' 2014-11-02 00:44:57 -07:00
Joe Nelson 822c9aad22 Cannot use query param for set role. Concat should be safe though 2014-11-02 00:23:24 -07:00
Joe Nelson 1a83f2d344 Stop using strings, they are dog slow 2014-11-02 00:10:37 -07:00
Joe Nelson 28da88829e Merge pull request #91 from begriffs/s3
Easy builds for Heroku
2014-10-28 20:09:32 -07:00
Joe Nelson 5453e1988c Document heroku build 2014-10-28 20:05:15 -07:00
Joe Nelson b09ca76333 Add optimization and s3 release script 2014-10-28 19:44:50 -07:00
Joe Nelson 12c1f0fa23 Merge pull request #90 from begriffs/server-header
Report server and version in headers
2014-10-28 17:30:57 -07:00
Joe Nelson 6a5b756cac Report server and version in headers 2014-10-28 17:17:37 -07:00
Joe Nelson aaf8a7fbfa Merge pull request #89 from begriffs/release
cleanup connections on close.
2014-10-28 15:26:27 -07:00
Adam C. Baker 2b829f9152 cleanup connections on close. 2014-10-28 14:41:32 -07:00
Joe Nelson f84ca1d615 Merge branch 'pool-opts'
Fixes #85
2014-10-21 16:21:01 -07:00
Joe Nelson 32e3396caf Provide command-line flag for db pool size 2014-10-21 16:15:11 -07:00
Joe Nelson 563187f3c0 Merge pull request #86 from begriffs/user-management
Route for adding a new user
2014-10-21 15:37:44 -07:00
Joe Nelson 05bf6b9b06 Standarize db schema dump format 2014-10-21 15:16:03 -07:00
Joe Nelson 572c235b79 Add /dbapi/users route for creating new user 2014-10-21 15:01:04 -07:00
Joe Nelson 5953936c7a Bump version 2014-10-21 10:51:03 -07:00
Joe Nelson dfa27bace1 Merge branch 'pools' 2014-10-17 23:50:38 -07:00
Joe Nelson 36eeec6b17 Retry the sandbox curl on travis as needed 2014-10-17 23:46:47 -07:00
Joe Nelson 25ce4c00c6 Fix hlint 2014-10-17 18:13:48 -07:00
Adam C. Baker 2773ef3030 restore test 2014-10-17 17:28:06 -07:00
Adam C. Baker 0798cb81a6 App has database pools! 2014-10-17 17:16:41 -07:00
Adam C. Baker 49f0bf7c3f broken attempts to work on concurrency 2014-10-17 17:10:34 -07:00
Joe Nelson 15d5143400 Merge branch 'json-parse' 2014-10-14 19:28:04 -07:00
Joe Nelson c163e2156c Indicate input json parse error with better message
Fixes #42
2014-10-14 19:23:56 -07:00
Joe Nelson be2f1f8e9e Merge pull request #83 from begriffs/404
Issue 404 for nonexistent tables
2014-10-14 19:02:06 -07:00
Joe Nelson 93c573ab4f Issue 404 for nonexistent tables
Fixes #44
2014-10-14 18:58:55 -07:00
Joe Nelson 8876e533ed Merge pull request #82 from begriffs/no-pending
All tests passing, none pending
2014-10-14 17:09:24 -07:00
Joe Nelson 37c0a2d831 All tests passing, none pending
Standardize the order of enum value lists
2014-10-14 15:03:07 -07:00
Joe Nelson 36b62de998 Merge pull request #79 from begriffs/favicon
Added a favicon, so important!
2014-10-14 13:47:15 -07:00
Joe Nelson e15043f01a Merge pull request #81 from begriffs/content-location
Fix Content-Location header when there are no params
2014-10-14 12:54:06 -07:00
Joe Nelson f1feaecf27 Fix Content-Location header when there are no params
Fixes #68
2014-10-14 11:49:36 -07:00
Joe Nelson f7751c45a2 Merge pull request #80 from begriffs/empty-response
Return [] rather than nothing on no results
2014-10-14 11:34:20 -07:00
Joe Nelson aa992f0d92 Add constraint to wai-middleware-static 2014-10-14 11:32:12 -07:00
Joe Nelson 21e4e583ec Return [] rather than nothing on no results
Fixes #70
2014-10-14 11:23:26 -07:00
Joe Nelson 17ada69411 Merge pull request #78 from begriffs/lookup
Remove getHeader, fixes #77
2014-10-14 11:14:48 -07:00
Joe Nelson 2338d000e1 Added a favicon, so important!
Fixes #71
2014-10-14 11:13:55 -07:00
Joe Nelson 80c55789b5 Remove getHeader, fixes #77 2014-10-14 10:30:23 -07:00
Joe Nelson 605c876bf5 Merge pull request #76 from begriffs/order-by
Accept order query param
2014-10-13 22:51:18 -07:00
Joe Nelson fbb13f4ab3 Accept order query param
Fixes #66
2014-10-13 22:29:28 -07:00
Joe Nelson 901df91d6c Merge pull request #75 from begriffs/errors
Errors
2014-10-13 18:23:59 -07:00
Joe Nelson 9c84cf9c3d Fix lint warnings 2014-10-13 18:16:41 -07:00
Adam C. Baker 5630ee52fd Merge branch 'master' into errors
Conflicts:
	src/Main.hs
	src/Middleware.hs
2014-10-13 17:45:32 -07:00
Adam C. Baker fd237b33b1 fix/unpend tests. 2014-10-13 16:54:21 -07:00
Adam C. Baker 1cee5b57ce fixing up the test app 2014-10-13 16:54:05 -07:00
Adam C. Baker f81ca8bf0b move auth out of dbapi, use middleware, put in main 2014-10-13 16:52:14 -07:00
Adam C. Baker fff84cca90 rename reportPgErrors to clientErrors 2014-10-13 16:51:21 -07:00
Adam C. Baker dcdca445b5 authenticated middleware 2014-10-13 16:48:20 -07:00
Adam C. Baker f7c825f646 rename pgSetRole, pgResetRole to setRole, resetRole 2014-10-13 16:46:33 -07:00
Adam C. Baker 2028500da7 withSavepoint middleware 2014-10-13 13:04:25 -07:00
Joe Nelson bd1d365143 Merge pull request #69 from begriffs/https-redirect
Remove internal TLS, add option for HTTPS redirection
2014-10-13 12:09:33 -07:00
Joe Nelson abccb295a3 Remove internal TLS, add option for HTTPS redirection 2014-10-12 19:52:20 -07:00
Adam C. Baker a984fe3bf9 flip args to inTransaction 2014-10-10 17:32:17 -07:00
Adam C. Baker e4669dcb37 handle all requests in a transaction. 2014-10-10 17:16:56 -07:00
Adam C. Baker 57f4d478fc pending failing options test. 2014-10-10 17:01:20 -07:00
Adam C. Baker a973020748 handle inserting no data correctly
with "insert into [table] default values"
2014-10-10 16:52:01 -07:00
Adam C. Baker 0879ed7110 flip app args 2014-10-10 15:54:05 -07:00
49 changed files with 3005 additions and 1477 deletions
+2
View File
@@ -5,3 +5,5 @@ cabal.sandbox.config
hscope.out
codex.tags
.anvil
.stack-work
tags
-22
View File
@@ -1,22 +0,0 @@
language: haskell
ghc: 7.8
addons:
postgresql: "9.3"
notifications:
slack: looprecur:z2nPS2Cfx2D33LE2obyU2FyQ
before_install:
- createuser --superuser --no-password dbapi_test
- createdb -O dbapi_test -U postgres dbapi_test
- travis_retry sudo add-apt-repository -y ppa:hvr/ghc
- travis_retry sudo apt-get update
- travis_retry sudo apt-get install --force-yes happy-1.19.3 alex-3.1.3
- export PATH=/opt/alex/3.1.3/bin:/opt/happy/1.19.3/bin:$PATH
install:
- curl http://bin.begriffs.com/dbapi/cabal-sandbox.tar.xz | tar xJ
- chmod a+x .cabal-sandbox/bin/*
- cabal sandbox init
- cabal install --enable-test --dependencies-only
- cabal install --enable-test
script:
- cabal test --show-details=always --test-options="--color"
- .cabal-sandbox/bin/hlint src/*.hs test/**/*.hs
+75
View File
@@ -0,0 +1,75 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [0.2.11.1] - 2015-09-01
### Fixed
- Accepts `*/*` in Accept header - @diogob
## [0.2.11.0] - 2015-08-28
### Added
- Negate any filter in a uniform way, e.g. `?col=not.eq=foo` - @diogob
- Call stored procedures
- Filter NOT IN values, e.g. `?col=notin.1,2,3` - @rall
- CSV responses to GET requests with `Accept: text/csv` - @diogob
- Debian init scripts - @mkhon
- Allow filters by computed columns - @diogob
### Fixed
- Reset user role on error
- Compatible with Stack
- Add materialized views to results in GET / - @diogob
- Indicate insertable=true for views that are insertable through triggers - @diogob
- Builds under GHC 7.10
- Allow the use of columns named "count" in relations queried - @diogob
## [0.2.10.0] - 2015-06-03
### Added
- Full text search, eg `/foo?text_vector=@@.bar`
- Include auth id as well as db role to views (for row-level security)
## [0.2.9.1] - 2015-05-20
### Fixed
- Put -Werror behind a cabal flag (for CI) so Hackage accepts package
## [0.2.9.0] - 2015-05-20
### Added
- Return range headers in PATCH
- Return PATCHed resources if header "Prefer: return=representation"
- Allow nested objects and arrays in JSON post for jsonb columns
- JSON Web Tokens - [Federico Rampazzo](https://github.com/framp)
- Expose PostgREST as a Haskell package
### Fixed
- Return 404 if no records updated by PATCH
## [0.2.8.0] - 2015-04-17
### Added
- Option to specify nulls first or last, eg `/people?order=age.desc.nullsfirst`
- Filter nulls, `?col=is.null` and `?col=isnot.null`
- Filter within jsonb, `?col->a->>b=eq.c`
- Accept CSV in post body for bulk inserts
### Fixed
- Allow NULL values in posts
- Show full command line usage on param errors
## [0.2.7.0] - 2015-03-03
### Added
- Server response logging
- Filter IN values, e.g. `?col=in.1,2,3`
- Return POSTed resource if header "Prefer: return=representation"
- Allow override of default (v1) schema
## [0.2.6.0] - 2015-02-18
### Added
- A changelog
- Filter by substring match, e.g. `?col=like.*hello*` (or ilike for
case insensitivity).
- Access-Control-Expose-Headers for CORS
### Fixed
- Make filter position match docs, e.g. `?order=col.asc` rather
than `?order=asc.col`.
+55
View File
@@ -0,0 +1,55 @@
# Contributing to PostgREST
**First:** if you're unsure or afraid of _anything_, just ask or
submit the issue or pull request anyways. You won't be yelled at
for giving your best effort. The worst that can happen is that
you'll be politely asked to change something. We appreciate any
sort of contributions, and don't want a wall of rules to get in the
way of that.
However, for those individuals who want a bit more guidance on the
best way to contribute to the project, read on. This document will
cover what we're looking for. By addressing all the points we're
looking for, it raises the chances we can quickly merge or address
your contributions.
## Issues
### Reporting an Issue
* Make sure you test against the latest released version. It is possible
we already fixed the bug you're experiencing.
* Also check the `CHANGELOG.md` to see if any unreleased changes affect
the issue. The very newest changes can take a little while to be released
as a new official version.
* Provide steps to reproduce the issue, including your OS version and
the specific database schema that you are using.
## Code
### Haskell Conventions
* All contributions must pass the tests before being merged. When
you create a pull request your code will automatically be tested.
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/)
with no warnings. This helps enforce a uniform style for all
committers. Continuous integration will check this as well on every
pull request.
* For help building the Haskell code on your computer check out the [building from
source](https://github.com/begriffs/postgrest/wiki/Building-from-source)
wiki page.
## Maintenance
### Schedule
Currently I (@begriffs) am the sole maintainer, and while I am
overjoyed to help resolve issues I also have to balance this with
my other obligations. If you don't get a response right away
don't worry, I will definitely get to it. Also you can join the
Gitter [chat room](https://gitter.im/begriffs/postgrest) to
discuss issues you are having.
+162 -30
View File
@@ -1,43 +1,175 @@
## Serve a RESTful API from any Postgres database
![Logo](static/logo.png "Logo")
[![Build Status](https://travis-ci.org/begriffs/dbapi.svg?branch=master)](https://travis-ci.org/begriffs/dbapi)
[![Build Status](https://circleci.com/gh/begriffs/postgrest.png?style=shield&circle-token=f723c01686abf0364de1e2eaae5aff1f68bd3ff2)](https://circleci.com/gh/begriffs/postgrest/tree/master)
<a href="https://heroku.com/deploy?template=https://github.com/begriffs/postgrest">
<img src="https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg" alt="Deploy">
</a>
[![Join the chat at https://gitter.im/begriffs/postgrest](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/begriffs/postgrest)
### Installation
PostgREST serves a fully RESTful API from any existing PostgreSQL
database. It provides a cleaner, more standards-compliant, faster
API than you are likely to write from scratch.
```sh
brew install postgres
cabal install -j --enable-tests
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Watch [Video](http://begriffs.com/posts/2014-12-30-intro-to-postgrest.html) | [GUI Demo](http://marmelab.com/ng-admin-postgrest)
Try making requests to the live demo server with an HTTP client
such as [postman](http://www.getpostman.com/). The structure of the
demo database is defined by
[begriffs/postgrest-example](https://github.com/begriffs/postgrest-example).
You can use it as inspiration for test-driven server migrations in
your own projects.
### Usage
Download the binary ([latest release](https://github.com/begriffs/postgrest/releases/latest)) and invoke like so:
```bash
postgrest --db-host localhost --db-port 5432 \
--db-name my_db --db-user postgres \
--db-pass foobar --db-pool 200 \
--anonymous postgres --port 3000 \
--v1schema public
```
Example usage:
In production include the `--secure` option which redirects all
requests to HTTPS. Note that PostgREST does not handle the SSL
internally and must be put behind another server that does (such
as nginx or the Heroku load balancer).
```sh
cabal run -p 3000 -d postgres://[auth-role]:@localhost:5432/[database] -a [anonymous-role]
```
### Performance
You will need to provide two database roles (which are allowed to
be the same). One is called the authenticator role (`auth-role`
above) which should have enough privileges to read the `auth` table
in the `dbapi` schema if you intend to support multi-user applications.
TLDR; subsecond response times for up to 2000 requests/sec on Heroku free tier. ([see the load test](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling))
The other role is for anonymous access (`anonymous-role` above).
Immediately upon acceping any unauthenticated HTTP connection dbapi
assumes this role in its queries to postgres. Give this role as
much or little permissions as you would like.
If you're used to servers written in interpreted languages (or named
after precious gems), prepare to be pleasantly surprised by PostgREST
performance.
### Running tests
Three factors contribute to the speed. First the server is written
in [Haskell](https://new-www.haskell.org/) using the
[Warp](http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks)
HTTP server (aka a compiled language with lightweight threads).
Next it delegates as much calculation as possible to the database
including
```sh
createuser --superuser --no-password dbapi_test
createdb -O dbapi_test -U postgres dbapi_test
* Serializing JSON responses directly in SQL
* Data validation
* Authorization
* Combined row counting and retrieval
* Data post in single command (`returning *`)
cabal test --show-details=always --test-options="--color"
```
Finally it uses the database efficiently with the
[Hasql](https://nikita-volkov.github.io/hasql-benchmarks/) library
by
### Accessing server on localhost
* Reusing prepared statements
* Keeping a pool of db connections
* Using the Postgres binary protocol
* Being stateless to allow horizontal scaling
Dbapi permits only secure https access. By default it uses a
self-signed key which will cause Chrome and other rest clients to
complain. You will need to configure your system to trust this key.
On mac follow [these
instructions](http://www.robpeck.com/2010/10/google-chrome-mac-os-x-and-self-signed-ssl-certificates/).
Ultimately the server (when load balanced) is constrained by database
performance. This may make it inappropriate for very large traffic
load. To learn more about scaling with Heroku and Amazon RDS see
the [performance guide](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling).
Other optimizations are possible, and some are outlined in the
[Future Features](#future-features).
### Security
PostgREST handles authentication (HTTP Basic over SSL or [JSON Web
Tokens](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions#json-web-tokens))
and delegates authorization to the role information defined in the
database. This ensures there is a single declarative source of truth
for security. When dealing with the database the server assumes
the identity of the currently authenticated user, and for the
duration of the connection cannot do anything the user themselves
couldn't.
Postgres 9.5 will soon support true [row-level
security](http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/).
In the meantime what isn't yet implemented can be simulated with
triggers and security-barrier views. Because the possible queries
to the database are limited to certain templates using
[leakproof](http://blog.2ndquadrant.com/how-do-postgresql-security_barrier-views-work/)
functions, the trigger workaround does not compromise row-level
security.
For example security patterns see the [security
guide](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions).
### Versioning
A robust long-lived API needs the freedom to exist in multiple
versions. PostgREST supports versioning through HTTP content
negotiation. Requests for a certain version translate into switching
which database schema to search for tables. PostgreSQL schema search
paths allow tables from earlier versions to be reused verbatim in
later versions.
To learn more, see the [guide to versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning).
### Self-documention
Rather than writing and maintaining separate docs yourself let the
API explain its own affordances using HTTP. All PostgREST endpoints
respond to the OPTIONS verb and explain what they support as well
as the data format of their JSON payload.
The number of rows returned by an endpoint is reported by - and
limited with - range headers. More about
[that](http://begriffs.com/posts/2014-03-06-beyond-http-header-links.html).
There are more opportunities for self-documentation listed in [Future
Features](#future-features).
### Data Integrity
Rather than relying on an Object Relational Mapper and custom
imperative coding, this system requires you put declarative constraints
directly into your database. Hence no application can corrupt your
data (including your API server).
The PostgREST exposes HTTP interface with safeguards to prevent
surprises, such as enforcing idempotent PUT requests, and
See examples of [Postgres
constraints](http://www.tutorialspoint.com/postgresql/postgresql_constraints.htm)
and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
### Future Features
* Watching endpoint changes with sockets and Postgres pubsub
* Specifying per-view HTTP caching
* Inferring good default caching policies from the Postgres stats collector
* Generating mock data for test clients
* Maintaining separate connection pools per role to avoid "set/reset
role" performance penalty
* Describe more relationships with Link headers
* Depending on accept headers, render OPTIONS as [RAML](http://raml.org/) or a
relational diagram
* Add two-legged auth with OAuth 1.0a(?)
* ... the other [issues](https://github.com/begriffs/postgrest/issues)
### Guides
* [Routing](https://github.com/begriffs/postgrest/wiki/Routing)
* [Versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning)
* [Performance](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling)
* [Security](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions)
* [Tutorial](http://blog.jonharrington.org/postgrest-introduction/) (external)
* [Heroku](https://github.com/begriffs/postgrest/wiki/Heroku)
### Thanks
* [Adam Baker](https://github.com/adambaker) for code
contributions and many fundamental design discussions
* [Diogo Biazus](https://github.com/diogob) for many improvements
and deep postgresql knowledge
* [Nikita Volkov](https://github.com/nikita-volkov) for writing the
wonderful [Hasql](https://github.com/nikita-volkov/hasql) library
and helping me use it
* [Mikey Casalaina](https://github.com/casalaina) for the cool logo
* [Jonathan Harrington](https://github.com/prio) for writing a [nice
tutorial](http://blog.jonharrington.org/postgrest-introduction/)
* [Federico Rampazzo](https://github.com/framp) for suggesting and
implementing [JWT](http://jwt.io/) support
+56
View File
@@ -0,0 +1,56 @@
{
"name": "PostgREST",
"description": "RESTful API for any PostgreSQL database.",
"logo": "https://halcyon.sh/logo.svg",
"repository": "https://github.com/begriffs/postgrest",
"env": {
"BUILDPACK_URL": {
"description": "Heroku buildpack for deploying Haskell applications",
"value": "https://github.com/begriffs/postgrest-heroku"
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "0.2.11.1"
},
"DB_NAME": {
"description": "Database name",
"required": true
},
"AUTH_ROLE": {
"description": "Database role to use checking client authentication",
"required": true
},
"AUTH_PASS": {
"description": "Authentication password",
"required": false
},
"ANONYMOUS_ROLE": {
"description": "Database role for non-authenticated requests",
"required": true
},
"DB_HOST": {
"description": "Database server hostname",
"required": true
},
"DB_PORT": {
"description": "Database server port",
"required": false,
"value": "5432"
},
"DB_POOL": {
"description": "Maximum number of connections in database pool",
"required": false,
"value": "10"
},
"JWT_SECRET": {
"description": "Secret used to encrypt JSON Web Tokens",
"required": false,
"value": "secret"
},
"V1SCHEMA": {
"description": "DB schema selected whe no version (or version 1) requested",
"required": false,
"value": "1"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
machine:
pre:
- createuser --superuser --no-password postgrest_test
- createdb -O postgrest_test -U ubuntu postgrest_test
ghc:
version: 7.8.3
dependencies:
override:
- cabal update
- cabal sandbox init
- cabal install --upgrade-dependencies --constraint="template-haskell installed" --dependencies-only --enable-tests
- cabal configure --enable-tests -f ci
test:
post:
- cabal exec hlint -- -X QuasiQuotes src/**/*.hs test/**/*.hs
- cabal exec packdeps postgrest.cabal
-69
View File
@@ -1,69 +0,0 @@
name: dbapi
version: 0.1.0.0
synopsis: The database is your api
license: MIT
license-file: LICENSE
author: Joe Nelson, Adam Baker
maintainer: cred+github@begriffs.com
category: Web
build-type: Simple
cabal-version: >=1.10
executable dbapi
main-is: Main.hs
ghc-options: -Wall -W -Werror
default-language: Haskell2010
build-depends: base >=4.6 && <5
, HDBC, HDBC-postgresql
, warp, wai >= 3.0.1 && < 3.0.2
, wai-extra, wai-cors
, HTTP, convertible
, case-insensitive
, http-types, scientific, time
, bytestring, aeson, network
, text , containers
, optparse-applicative >= 0.9.1 && < 0.10
, unordered-containers
, regex-base
, string-conversions
, http-media, regex-tdfa
, Ranged-sets
, transformers
, warp-tls
, bcrypt
, base64-string
, split
Other-Modules: Dbapi
, PgStructure
, PgQuery
, RangeQuery
, Middleware
hs-source-dirs: src
Test-Suite spec
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
Hs-Source-Dirs: test, src
ghc-options: -Wall -W -Werror
Main-Is: Main.hs
Other-Modules: Dbapi, Spec, SpecHelper
Build-Depends: base, hspec2
, hspec-wai >= 0.5.0, hspec-wai-json
, HDBC, HDBC-postgresql
, warp, wai >= 3.0.1 && < 3.0.2
, HTTP, convertible
, case-insensitive
, wai-extra, wai-cors, containers
, http-types, scientific, time
, bytestring, aeson, network
, text, optparse-applicative
, unordered-containers
, regex-base
, string-conversions
, http-media, regex-tdfa
, Ranged-sets
, transformers
, warp-tls
, bcrypt
, base64-string
, split
Vendored Executable
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
d=$(dirname $0)
if [ -f /etc/default/postgrest ]; then
. /etc/default/postgrest
fi
POSTGREST_LOG=${POSTGREST_LOG:-/var/log/postgrest/postgrest.log}
exec $d/postgrest "$@" >>$POSTGREST_LOG 2>&1 &
+23
View File
@@ -0,0 +1,23 @@
# run service as
#POSTGREST_USER=postgrest
# log file
#POSTGREST_LOG=/var/log/postgrest/postgrest.log
# database host
#POSTGREST_DBHOST=localhost
# database to use
#POSTGREST_DBNAME=
# database user
#POSTGREST_DBUSER=postgres
# database password
#POSTGREST_DBPASS=
# database pool
#POSTGREST_DBPOOL=10
# additional options
#POSTGREST_OPTS=
Vendored Executable
+78
View File
@@ -0,0 +1,78 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: postgrest
# Required-Start: $local_fs $network postgresql
# Required-Stop: $local_fs $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: PostgreSQL REST API daemon
### END INIT INFO
. /lib/lsb/init-functions
if test -f /etc/default/postgrest; then
. /etc/default/postgrest
fi
POSTGREST=/usr/local/bin/postgrest
POSTGREST_USER=${POSTGREST_USER:-postgrest}
POSTGREST_DBNAME=${POSTGREST_DBNAME:-postgres}
POSTGREST_DBUSER=${POSTGREST_DBUSER:-postgres}
if [ -n "$POSTGREST_DBHOST" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-host $POSTGREST_DBHOST"
fi
if [ -n "$POSTGREST_DBNAME" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-name $POSTGREST_DBNAME"
fi
if [ -n "$POSTGREST_DBUSER" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-user $POSTGREST_DBUSER"
POSTGREST_OPTS="$POSTGREST_OPTS --anonymous $POSTGREST_DBUSER"
fi
if [ -n "$POSTGREST_DBPASS" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-pass $POSTGREST_DBPASS"
fi
if [ -n "$POSTGREST_DBPOOL" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-pool $POSTGREST_DBPOOL"
fi
POSTGREST_OPTS="$POSTGREST_OPTS --v1schema public"
start()
{
log_daemon_msg "Starting PostgreSQL REST API daemon" "postgrest" || true
if start-stop-daemon --start --quiet --oknodo --chuid ${POSTGREST_USER} --startas /usr/local/bin/postgrest-wrapper --exec $POSTGREST -- $POSTGREST_OPTS; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
}
stop()
{
log_daemon_msg "Stopping PostgreSQL REST API daemon" "postgrest" || true
if start-stop-daemon --stop --quiet --oknodo --exec $POSTGREST; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
}
status()
{
status_of_proc $POSTGREST postgrest && exit 0 || exit $?
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
esac
+149
View File
@@ -0,0 +1,149 @@
name: postgrest
description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security
permits.
version: 0.2.11.1
synopsis: REST API for any Postgres database
license: MIT
license-file: LICENSE
author: Joe Nelson, Adam Baker
homepage: https://github.com/begriffs/postgrest
maintainer: cred+github@begriffs.com
category: Web
build-type: Simple
cabal-version: >=1.10
source-repository head
type: git
location: git://github.com/begriffs/postgrest.git
Flag CI
Description: No warnings allowed in continuous integration
Manual: True
Default: False
executable postgrest
main-is: PostgREST/Main.hs
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
default-language: Haskell2010
build-depends: base >=4.6 && <5
, postgrest
, hasql >= 0.7.3 && < 0.8
, hasql-backend >= 0.4.1 && < 0.5
, hasql-postgres >= 0.10.4 && < 0.11
, warp >= 3.0.2, wai >= 3.0.1
, wai-extra, wai-cors
, wai-middleware-static >= 0.6.0
, HTTP, convertible, http-types
, case-insensitive
, scientific, time
, aeson >= 0.8, network >= 2.6
, bytestring, text, split, string-conversions
, stringsearch
, containers, unordered-containers
, optparse-applicative == 0.11.*
, regex-base, regex-tdfa
, Ranged-sets
, transformers, MissingH
, bcrypt >= 0.0.6, base64-string
, network-uri >= 2.6
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, jwt
hs-source-dirs: src
library
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
default-language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
build-depends: base >=4.6 && <5
, hasql, hasql-backend
, hasql-postgres
, warp, wai
, wai-extra, wai-cors
, wai-middleware-static
, HTTP, convertible, http-types
, case-insensitive
, scientific, time
, aeson, network
, bytestring, text, split, string-conversions
, stringsearch
, containers, unordered-containers
, optparse-applicative
, regex-base, regex-tdfa
, Ranged-sets
, transformers, MissingH
, bcrypt, base64-string
, network-uri
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, jwt
Exposed-Modules: PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.PgQuery
, PostgREST.PgStructure
, PostgREST.RangeQuery
hs-source-dirs: src
Test-Suite spec
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
Hs-Source-Dirs: test, src
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
Main-Is: Main.hs
Other-Modules: PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.PgQuery
, PostgREST.PgStructure
, PostgREST.RangeQuery
, Spec
, SpecHelper
Build-Depends: base, hspec == 2.1.*, QuickCheck
, hspec-wai, hspec-wai-json
, hasql, hasql-backend
, hasql-postgres
, warp, wai
, packdeps, hlint
, HTTP, convertible
, case-insensitive
, wai-extra, wai-cors, containers
, wai-middleware-static
, http-types, scientific, time
, bytestring, aeson, network
, text, optparse-applicative
, stringsearch
, unordered-containers
, regex-base
, string-conversions
, http-media, regex-tdfa
, Ranged-sets
, transformers, MissingH, split
, bcrypt, base64-string
, network-uri
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, process
, heredoc
, jwt
+10
View File
@@ -0,0 +1,10 @@
export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'`
curl -L http://sourceforge.net/projects/s3tools/files/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx
cp /app/dist/build/postgrest/postgrest postgrest-${POSTGREST_VER}
tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER}
touch ~/.s3cfg
s3cmd-1.5.0-alpha1/s3cmd put --access_key=${S3_ACCESS_KEY} --secret_key=${S3_SECRET_KEY} -P -f postgrest-${POSTGREST_VER}.tar.xz $S3_BUCKET/postgrest-${POSTGREST_VER}.tar.xz
-234
View File
@@ -1,234 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- {{{ Imports
module Dbapi where
import Types (SqlRow, getRow)
import Control.Monad (join)
import Control.Exception.Base (bracket_)
import Control.Arrow ((***))
import Control.Applicative
import Options.Applicative hiding (columns)
import Data.Maybe (fromMaybe, isJust)
import Text.Regex.TDFA ((=~))
import Data.Map (intersection, fromList, toList, Map)
import Data.List (sort)
import qualified Data.Set as S
import Data.Convertible.Base (convert)
import Data.Text (strip)
import Network.HTTP.Types.Status
import Network.HTTP.Types.Header
import Network.HTTP.Types.URI
import Network.HTTP.Base (urlEncodeVars)
import Network.Wai
import Network.Wai.Internal
import Network.Wai.Middleware.Cors (CorsResourcePolicy(..))
import qualified Data.ByteString.Char8 as BS
import Data.String.Conversions (cs)
import qualified Data.CaseInsensitive as CI
import Database.HDBC.PostgreSQL (Connection)
import PgStructure (printTables, printColumns, primaryKeyColumns,
columns, Column(colName))
import qualified Data.Aeson as JSON
import PgQuery
import RangeQuery
import Data.Ranged.Ranges (emptyRange)
import Codec.Binary.Base64.String (decode)
-- }}}
data AppConfig = AppConfig {
configDbUri :: String
, configPort :: Int
, configSslCert :: FilePath
, configSslKey :: FilePath
, configAnonRole :: String
}
jsonContentType :: (HeaderName, BS.ByteString)
jsonContentType = (hContentType, "application/json")
jsonBodyAction :: Request -> (SqlRow -> IO Response) -> IO Response
jsonBodyAction req handler = do
parse <- jsonBody req
case parse of
Left err -> return $ responseLBS status400 [jsonContentType] json
where json = JSON.encode . JSON.object $ [("error", JSON.String $ cs err)]
Right body -> handler body
jsonBody :: Request -> IO (Either String SqlRow)
jsonBody = fmap JSON.eitherDecode . strictRequestBody
filterByKeys :: Ord a => Map a b -> [a] -> Map a b
filterByKeys m keys =
if null keys then m else
m `intersection` fromList (zip keys $ repeat undefined)
httpRequesterRole :: RequestHeaders -> Connection -> IO LoginAttempt
httpRequesterRole hdrs conn = do
let auth = fromMaybe "" $ lookup hAuthorization hdrs
case BS.split ' ' (cs auth) of
("Basic" : b64 : _) ->
case BS.split ':' $ cs (decode $ cs b64) of
(u:p:_) -> signInRole u p conn
_ -> return MalformedAuth
_ -> return NoCredentials
app :: Connection -> DbRole -> Application
app conn anonymous req respond = do
attempt <- httpRequesterRole (requestHeaders req) conn
case attempt of
MalformedAuth ->
respond $ responseLBS status400 [] "Malformed basic auth header"
LoginFailed ->
respond $ responseLBS status401 [] "Invalid username or password"
LoginSuccess role ->
bracket_ (pgSetRole conn role) (pgResetRole conn) $ appWithRole conn req respond
NoCredentials ->
bracket_ (pgSetRole conn anonymous) (pgResetRole conn) $ appWithRole conn req respond
appWithRole :: Connection -> Application
appWithRole conn req respond =
respond =<< case (path, verb) of
([], _) ->
responseLBS status200 [jsonContentType] <$> printTables ver conn
([table], "OPTIONS") ->
responseLBS status200 [jsonContentType, allOrigins] <$>
printColumns ver (cs table) conn
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else do
r <- respondWithRangedResult <$> getRows ver (cs table) qq range conn
let canonical = urlEncodeVars $ sort $
map (join (***) cs) $
parseSimpleQuery $
rawQueryString req
return $ addHeaders [
("Content-Location",
"/" <> cs table <> "?" <> cs canonical
)] r
([table], "POST") ->
jsonBodyAction req (\row -> do
allvals <- insert ver table row conn
keys <- primaryKeyColumns ver (cs table) conn
let params = urlEncodeVars $ map (\t -> (fst t, "eq." <> convert (snd t) :: String)) $ toList $ filterByKeys allvals keys
return $ responseLBS status201
[ jsonContentType
, (hLocation, "/" <> cs table <> "?" <> cs params)
] ""
)
([table], "PUT") ->
jsonBodyAction req (\row -> do
keys <- primaryKeyColumns ver (cs table) conn
let specifiedKeys = map (cs . fst) qq
if S.fromList keys /= S.fromList specifiedKeys
then return $ responseLBS status405 []
"You must speficy all and only primary keys as params"
else
if isJust cRange
then return $ responseLBS status400 []
"Content-Range is not allowed in PUT request"
else do
cols <- columns ver (cs table) conn
let colNames = S.fromList $ map (cs . colName) cols
let specifiedCols = S.fromList $ map fst $ getRow row
return $ if colNames == specifiedCols then
responseLBS status200 [ jsonContentType ] ""
else if S.null colNames then responseLBS status404 [] ""
else responseLBS status400 []
"You must specify all columns in PUT request"
)
(_, _) ->
return $ responseLBS status404 [] ""
where
path = pathInfo req
verb = requestMethod req
qq = queryString req
hdrs = requestHeaders req
ver = fromMaybe "1" $ requestedVersion hdrs
range = requestedRange hdrs
cRange = requestedContentRange hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
(Just $ 60*60*24) False False True
corsPolicy :: Request -> Maybe CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of
Just origin -> Just defaultCorsPolicy {
corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders
}
Nothing -> Nothing
where
headers = requestHeaders req
accHeaders = case lookup "access-control-request-headers" headers of
Just hdrs -> map (CI.mk . cs . strip . cs) $ BS.split ',' hdrs
Nothing -> []
respondWithRangedResult :: RangedResult -> Response
respondWithRangedResult rr =
responseLBS status [
jsonContentType,
("Content-Range",
if total == 0 || from > total
then "*/" <> cs (show total)
else cs (show from) <> "-"
<> cs (show to) <> "/"
<> cs (show total)
)
] (rrBody rr)
where
from = rrFrom rr
to = rrTo rr
total = rrTotal rr
status
| from > total = status416
| total == 0 = status204
| (1 + to - from) < total = status206
| otherwise = status200
requestedVersion :: RequestHeaders -> Maybe String
requestedVersion hdrs =
case verStr of
Just [[_, ver]] -> Just ver
_ -> Nothing
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
accept = cs <$> lookup hAccept hdrs :: Maybe String
verStr = (=~ verRegex) <$> accept :: Maybe [[String]]
addHeaders :: ResponseHeaders -> Response -> Response
addHeaders hdrs (ResponseFile s headers fp m) =
ResponseFile s (headers ++ hdrs) fp m
addHeaders hdrs (ResponseBuilder s headers b) =
ResponseBuilder s (headers ++ hdrs) b
addHeaders hdrs (ResponseStream s headers b) =
ResponseStream s (headers ++ hdrs) b
addHeaders hdrs (ResponseRaw s resp) =
ResponseRaw s (addHeaders hdrs resp)
-47
View File
@@ -1,47 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- {{{ Imports
module Main where
import Dbapi
import Middleware (reportPgErrors)
import Network.Wai.Handler.Warp hiding (Connection)
import Database.HDBC.PostgreSQL (connectPostgreSQL')
import Data.String.Conversions (cs)
import Control.Applicative
import Options.Applicative hiding (columns)
import Network.Wai.Handler.WarpTLS (tlsSettings, runTLS)
import Network.Wai.Middleware.Gzip (gzip, def)
import Network.Wai.Middleware.Cors (cors)
-- }}}
argParser :: Parser AppConfig
argParser = AppConfig
<$> strOption (long "db" <> short 'd' <> metavar "URI"
<> help "database uri to expose, e.g. postgres://user:pass@host:port/database")
<*> option (long "port" <> short 'p' <> metavar "NUMBER" <> value 3000
<> help "port number on which to run HTTP server")
<*> strOption (long "sslcert" <> short 'c' <> metavar "PATH" <> value "test/test.crt"
<> help "path to SSL cert file")
<*> strOption (long "sslkey" <> short 'k' <> metavar "PATH" <> value "test/test.key"
<> help "path to SSL key file")
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE"
<> help "postgres role to use for non-authenticated requests")
main :: IO ()
main = do
conf <- execParser (info (helper <*> argParser) describe)
let port = configPort conf
let dburi = configDbUri conf
let tls = tlsSettings (configSslCert conf) (configSslKey conf)
let settings = setPort port defaultSettings
Prelude.putStrLn $ "Listening on port " ++ (show $ configPort conf :: String)
conn <- connectPostgreSQL' dburi
runTLS tls settings $ gzip def $ cors corsPolicy $ reportPgErrors $ app conn (cs $ configAnonRole conf)
where
describe = progDesc "create a REST API to an existing Postgres database"
-33
View File
@@ -1,33 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Middleware where
import Data.Aeson
import Network.HTTP.Types.Header (hContentType)
import Network.HTTP.Types.Status (status400)
import Database.HDBC.Types (SqlError(..))
import Control.Exception (catchJust)
import Network.Wai
instance ToJSON SqlError where
toJSON t = object [
"error" .= object [
"code" .= seNativeError t
, "message" .= seErrorMsg t
, "state" .= seState t
]
]
reportPgErrors :: Middleware
reportPgErrors app req respond =
catchJust isPgException (app req respond) (
respond . responseLBS status400 [(hContentType, "application/json")]
. encode
)
where
isPgException :: SqlError -> Maybe SqlError
isPgException = Just
-210
View File
@@ -1,210 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- {{{ Imports
module PgQuery (
getRows
, insert
, upsert
, addUser
, signInRole
, pgSetRole
, pgResetRole
, checkPass
, RangedResult(..)
, LoginAttempt(..)
, DbRole
) where
import Data.Text (Text)
import Data.String.Conversions (cs)
import Data.Functor ( (<$>) )
import Data.Maybe (fromMaybe)
import Data.List (intersperse, intercalate)
import Data.Monoid ((<>), mconcat)
import qualified Data.Map as M
import Control.Monad (join, void)
import qualified RangeQuery as R
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import Database.HDBC hiding (colType, colNullable)
import Database.HDBC.PostgreSQL
import qualified Network.HTTP.Types.URI as Net
import Types (SqlRow(..), getRow, sqlRowColumns, sqlRowValues)
import Crypto.BCrypt (hashPasswordUsingPolicy, fastBcryptHashingPolicy, validatePassword)
-- }}}
data RangedResult = RangedResult {
rrFrom :: Int
, rrTo :: Int
, rrTotal :: Int
, rrBody :: BL.ByteString
} deriving (Show)
type QuotedSql = (String, [SqlValue])
type Schema = String
type DbRole = BS.ByteString
data LoginAttempt =
NoCredentials
| MalformedAuth
| LoginFailed
| LoginSuccess DbRole
deriving (Eq, Show)
getRows :: Schema -> String -> Net.Query -> Maybe R.NonnegRange -> Connection -> IO RangedResult
getRows schema table qq range conn = do
query <- populateSql conn
$ globalAndLimitedCounts schema table qq <>
jsonArrayRows
(selectStarClause schema table
<> whereClause qq
<> limitClause range)
r <- quickQuery conn query []
return $ case r of
[[total, _, SqlNull]] -> RangedResult offset 0 (fromSql total) ""
[[total, limited_total, json]] ->
RangedResult offset (offset + fromSql limited_total - 1)
(fromSql total) (fromSql json)
_ -> RangedResult 0 0 0 ""
where
offset = fromMaybe 0 $ R.offset <$> range
whereClause :: Net.Query -> QuotedSql
whereClause qs =
if null qs then ("", []) else (" where ", []) <> conjunction
where
conjunction = mconcat $ intersperse (" and ", []) (map wherePred qs)
wherePred :: Net.QueryItem -> QuotedSql
wherePred (column, predicate) =
("%I " <> op <> "%L", map toSql [column, value])
where
opCode:rest = BS.split '.' $ fromMaybe "." predicate
value = BS.intercalate "." rest
op = case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
_ -> "="
limitClause :: Maybe R.NonnegRange -> QuotedSql
limitClause range =
(" LIMIT %s OFFSET %s ", [toSql limit, toSql offset])
where
limit = fromMaybe "ALL" $ show <$> (R.limit =<< range)
offset = fromMaybe 0 $ R.offset <$> range
globalAndLimitedCounts :: Schema -> String -> Net.Query -> QuotedSql
globalAndLimitedCounts schema table qq =
(" select ", [])
<> ("(select count(1) from %I.%I ", map toSql [schema, table])
<> whereClause qq
<> ("), count(t), ", [])
selectStarClause :: Schema -> String -> QuotedSql
selectStarClause schema table =
(" select * from %I.%I ", map toSql [schema, table])
jsonArrayRows :: QuotedSql -> QuotedSql
jsonArrayRows q =
("array_to_json(array_agg(row_to_json(t))) from (", []) <> q <> (") t", [])
insert :: Schema -> Text -> SqlRow -> Connection -> IO (M.Map String SqlValue)
insert schema table row conn = do
sql <- populateSql conn $ insertClause schema table row
stmt <- prepare conn sql
_ <- execute stmt $ sqlRowValues row
Just m <- fetchRowMap stmt
return m
addUser :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Connection -> IO ()
addUser identity pass role conn = do
hashed <- hashPasswordUsingPolicy fastBcryptHashingPolicy $ cs pass
_ <- insert "dbapi" "auth" (SqlRow [
("id", toSql identity), ("pass", toSql hashed), ("rolname", toSql role)
]) conn
return ()
signInRole :: BS.ByteString -> BS.ByteString -> Connection -> IO LoginAttempt
signInRole user pass conn = do
u <- quickQuery conn "select pass, rolname from dbapi.auth where id = ?" [toSql user]
return $ case u of
[[hashed, role]] ->
if checkPass (fromSql hashed) (cs pass)
then LoginSuccess $ fromSql role
else LoginFailed
_ -> LoginFailed
checkPass :: BS.ByteString -> BS.ByteString -> Bool
checkPass = validatePassword
upsert :: Schema -> Text -> SqlRow -> Net.Query -> Connection -> IO (M.Map String SqlValue)
upsert schema table row qq conn = do
sql <- populateSql conn $ upsertClause schema table row qq
stmt <- prepare conn sql
_ <- execute stmt $ join $ replicate 2 $ sqlRowValues row
Just m <- fetchRowMap stmt
return m
placeholders :: String -> SqlRow -> String
placeholders symbol = intercalate ", " . map (const symbol) . getRow
insertClause :: Schema -> Text -> SqlRow -> QuotedSql
insertClause schema table row =
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
map toSql $ cs schema : table : sqlRowColumns row)
<> (" values (" ++ placeholders "?" row ++ ") returning *", sqlRowValues row)
insertClauseViaSelect :: Schema -> Text -> SqlRow -> QuotedSql
insertClauseViaSelect schema table row =
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
map toSql $ cs schema : table : sqlRowColumns row)
<> (" select " ++ placeholders "?" row, sqlRowValues row)
updateClause :: Schema -> Text -> SqlRow -> QuotedSql
updateClause schema table row =
("update %I.%I set (" ++ placeholders "%I" row ++ ")",
map toSql $ cs schema : table : sqlRowColumns row)
<> (" = (" ++ placeholders "?" row ++ ")", [])
upsertClause :: Schema -> Text -> SqlRow -> Net.Query -> QuotedSql
upsertClause schema table row qq =
("with upsert as (", []) <> updateClause schema table row
<> whereClause qq
<> (" returning *) ", []) <> insertClauseViaSelect schema table row
<> (" where not exists (select * from upsert) returning *", [])
populateSql :: Connection -> QuotedSql -> IO String
populateSql conn sql = do
[[escaped]] <- quickQuery conn q (snd sql)
return $ fromSql escaped
where
q = concat [ "select format('", fst sql, "', ", ph (snd sql), ")" ]
ph :: [a] -> String
ph = intercalate ", " . map (const "?::varchar")
pgSetRole :: Connection -> DbRole -> IO ()
pgSetRole conn role = do
query <- populateSql conn ("set role %I", [toSql role])
void $ run conn query []
pgResetRole :: Connection -> IO ()
pgResetRole conn = void $ run conn "reset role" []
-193
View File
@@ -1,193 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module PgStructure where
import Data.Functor ( (<$>) )
import Data.Maybe (mapMaybe)
import Control.Applicative ( (<*>) )
import qualified Data.ByteString.Lazy as BL
import Data.List.Split (splitOn)
import qualified Data.Aeson as JSON
import qualified Data.Map as Map
import Database.HDBC hiding (colType, colNullable)
import Database.HDBC.PostgreSQL
import Data.Aeson ((.=))
data Table = Table {
tableSchema :: String
, tableName :: String
, tableInsertable :: Bool
} deriving (Show)
instance JSON.ToJSON Table where
toJSON v = JSON.object [
"schema" .= tableSchema v
, "name" .= tableName v
, "insertable" .= tableInsertable v ]
toBool :: String -> Bool
toBool = (== "YES")
data ForeignKey = ForeignKey {
fkTable::String, fkCol::String
} deriving (Eq, Show)
instance JSON.ToJSON ForeignKey where
toJSON fk = JSON.object ["table".=fkTable fk, "column".=fkCol fk]
foreignKeys :: String -> String -> Connection -> IO (Map.Map String ForeignKey)
foreignKeys schema table conn = do
r <- quickQuery conn
"select kcu.column_name, ccu.table_name AS foreign_table_name,\
\ ccu.column_name AS foreign_column_name \
\from information_schema.table_constraints AS tc \
\ join information_schema.key_column_usage AS kcu \
\ on tc.constraint_name = kcu.constraint_name \
\ join information_schema.constraint_column_usage AS ccu \
\ on ccu.constraint_name = tc.constraint_name \
\where constraint_type = 'FOREIGN KEY' \
\ and tc.table_name=? and tc.table_schema = ? \
\order by kcu.column_name" (map toSql [table, schema])
return $ foldl addKey Map.empty $ map (map fromSql) r
where
addKey m [col, ftab, fcol] = Map.insert col (ForeignKey ftab fcol) m
addKey m _ = m --should never happen
data Column = Column {
colSchema :: String
, colTable :: String
, colName :: String
, colPosition :: Int
, colNullable :: Bool
, colType :: String
, colUpdatable :: Bool
, colMaxLen :: Maybe Int
, colPrecision :: Maybe Int
, colDefault :: Maybe String
, colEnum :: Maybe [String]
, colFK :: Maybe ForeignKey
} deriving (Show)
instance JSON.ToJSON Column where
toJSON c = JSON.object [
"schema" .= colSchema c
, "name" .= colName c
, "position" .= colPosition c
, "nullable" .= colNullable c
, "type" .= colType c
, "updatable" .= colUpdatable c
, "maxLen" .= colMaxLen c
, "precision" .= colPrecision c
, "references".= colFK c
, "default" .= colDefault c
, "enum" .= colEnum c ]
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [String]
}
instance JSON.ToJSON TableOptions where
toJSON t = JSON.object [
"columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ]
tables :: String -> Connection -> IO [Table]
tables s conn = do
r <- quickQuery conn
"select table_schema, table_name,\
\ is_insertable_into\
\ from information_schema.tables\
\ where table_schema = ?\
\ order by table_name" [toSql s]
return $ mapMaybe mkTable r
where
mkTable [schema, name, insertable] =
Just $ Table (fromSql schema)
(fromSql name)
(toBool (fromSql insertable))
mkTable _ = Nothing
columns :: String -> String -> Connection -> IO [Column]
columns s t conn = do
r <- quickQuery conn
"select info.table_schema as schema, info.table_name as table_name, \
\ info.column_name as name, info.ordinal_position as position, \
\ info.is_nullable as nullable, info.data_type as col_type, \
\ info.is_updatable as updatable, \
\ info.character_maximum_length as max_len, \
\ info.numeric_precision as precision, \
\ info.column_default as default_value, \
\ array_to_string(enum_info.vals, ',') as enum \
\ from ( \
\ select table_schema, table_name, column_name, ordinal_position, \
\ is_nullable, data_type, is_updatable, \
\ character_maximum_length, numeric_precision, \
\ column_default, udt_name \
\ from information_schema.columns \
\ where table_schema = ? and table_name = ? \
\ ) as info \
\ left outer join ( \
\ select n.nspname as s, \
\ t.typname as n, \
\ array_agg(e.enumlabel) as vals \
\ from pg_type t \
\ join pg_enum e on t.oid = e.enumtypid \
\ join pg_catalog.pg_namespace n ON n.oid = t.typnamespace \
\ group by s, n \
\ ) as enum_info \
\ on (info.udt_name = enum_info.n) \
\order by position" [toSql s, toSql t]
fks <- foreignKeys s t conn
let lookupFK (_:_:name:_) = Map.lookup (fromSql name) fks
lookupFK _ = Nothing
let cols = zipWith ($) (map mkColumn r) (map lookupFK r)
return cols
where
mkColumn [schema, table, name, pos, nullable, colT, updatable, maxlen, precision, defVal, enum] = Column (fromSql schema)
(fromSql table)
(fromSql name)
(fromSql pos)
(toBool (fromSql nullable))
(fromSql colT)
(toBool (fromSql updatable))
(fromSql maxlen)
(fromSql precision)
(fromSql defVal)
(splitOn "," <$> fromSql enum)
mkColumn _ = error $ "Incomplete column data received for table " ++
t ++ " in schema " ++ s ++ "."
printTables :: String -> Connection -> IO BL.ByteString
printTables schema conn = JSON.encode <$> tables schema conn
printColumns :: String -> String -> Connection -> IO BL.ByteString
printColumns schema table conn =
JSON.encode <$> (TableOptions <$> cols <*> pkey)
where
cols :: IO [Column]
cols = columns schema table conn
pkey :: IO [String]
pkey = primaryKeyColumns schema table conn
primaryKeyColumns :: String -> String -> Connection -> IO [String]
primaryKeyColumns s t conn = do
r <- quickQuery conn
"select kc.column_name \
\ from \
\ information_schema.table_constraints tc, \
\ information_schema.key_column_usage kc \
\where \
\ tc.constraint_type = 'PRIMARY KEY' \
\ and kc.table_name = tc.table_name and kc.table_schema = tc.table_schema \
\ and kc.constraint_name = tc.constraint_name \
\ and kc.table_schema = ? \
\ and kc.table_name = ?" [toSql s, toSql t]
return $ map fromSql (concat r)
+362
View File
@@ -0,0 +1,362 @@
{-# LANGUAGE FlexibleContexts #-}
module PostgREST.App (app, sqlError, isSqlError, contentTypeForAccept) where
import Control.Monad (join)
import Control.Arrow ((***), second)
import Control.Applicative
import Data.Text hiding (map, find)
import Data.Maybe (fromMaybe, mapMaybe, isJust, isNothing)
import Text.Regex.TDFA ((=~))
import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange)
import qualified Data.HashMap.Strict as M
import Data.String.Conversions (cs)
import Data.CaseInsensitive (original)
import Data.List (sortBy, find)
import Data.Functor.Identity
import qualified Data.Set as S
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Char8 as BS
import qualified Blaze.ByteString.Builder as BB
import qualified Data.Csv as CSV
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 Network.Wai.Parse (parseHttpAccept)
import Network.Wai.Internal (Response(..))
import Data.Aeson
import Data.Monoid
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import PostgREST.Config (AppConfig(..))
import PostgREST.Auth
import PostgREST.PgQuery
import PostgREST.RangeQuery
import PostgREST.PgStructure
import Prelude
app :: AppConfig -> BL.ByteString -> Request -> H.Tx P.Postgres s Response
app conf reqBody req =
case (path, verb) of
([], _) -> do
body <- encode <$> tables (cs schema)
return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do
let qt = qualify table
cols <- columns qt
pkey <- map cs <$> primaryKeyColumns qt
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 = qualify table
from = fromMaybe 0 $ rangeOffset <$> range
select = B.Stmt "select " V.empty True <>
parentheticT (
whereT qt qq $ countRows qt
) <> commaq <> (
bodyForAccept contentType qt
. limitT range
. orderT (orderParse qq)
. whereT qt qq
$ selectStar qt
)
row <- H.maybeEx select
let (tableTotal, queryTotal, body) =
fromMaybe (0, 0, Just "" :: Maybe Text) row
to = from+queryTotal-1
contentRange = contentRangeH from to tableTotal
status = rangeStatus from to tableTotal
canonical = urlEncodeVars
. sortBy (comparing fst)
. map (join (***) cs)
. parseSimpleQuery
$ rawQueryString req
return $ responseLBS status
[contentTypeH, contentRange,
("Content-Location",
"/" <> cs table <>
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (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))
] ""
(["postgrest", "tokens"], "POST") ->
case jwtSecret of
"secret" -> return $ responseLBS status500 [jsonH] $
encode . object $ [("message", String "JWT Secret is set as \"secret\" which is an unsafe default.")]
_ -> do
let user = decode reqBody :: Maybe AuthUser
case user of
Nothing -> return $ responseLBS status400 [jsonH] $
encode . object $ [("message", String "Failed to parse user.")]
Just u -> do
setRole authenticator
login <- signInRole (cs $ userId u)
(cs $ userPass u)
case login of
LoginSuccess role uid ->
return $ responseLBS status201 [ jsonH ] $
encode . object $ [("token", String $ tokenJWT jwtSecret uid role)]
_ -> return $ responseLBS status401 [jsonH] $
encode . object $ [("message", String "Failed authentication.")]
([table], "POST") -> do
let qt = qualify table
echoRequested = lookupHeader "Prefer" == Just "return=representation"
parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value))
parsed = if lookupHeader "Content-Type" == Just csvMT
then do
rows <- CSV.decode CSV.NoHeader reqBody
if V.null rows then Left "CSV requires header"
else Right (V.head rows, (V.map $ V.map $ parseCsvCell . cs) (V.tail rows))
else eitherDecode reqBody >>= \val ->
case val of
Object obj -> Right . second V.singleton . V.unzip . V.fromList $
M.toList obj
_ -> Left "Expecting single JSON object or CSV rows"
case parsed of
Left err -> return $ responseLBS status400 [] $
encode . object $ [("message", String $ "Failed to parse JSON payload. " <> cs err)]
Right toBeInserted -> do
rows :: [Identity Text] <- H.listEx $ uncurry (insertInto qt) toBeInserted
let inserted :: [Object] = mapMaybe (decode . cs . runIdentity) rows
primaryKeys <- primaryKeyColumns qt
let responses = flip map inserted $ \obj -> do
let primaries =
if Prelude.null primaryKeys
then obj
else M.filterWithKey (const . (`elem` primaryKeys)) obj
let params = urlEncodeVars
$ map (\t -> (cs $ fst t, cs (paramFilter $ snd t)))
$ sortBy (comparing fst) $ M.toList primaries
responseLBS status201
[ jsonH
, (hLocation, "/" <> cs table <> "?" <> cs params)
] $ if echoRequested then encode obj else ""
return $ multipart status201 responses
(["rpc", proc], "POST") -> do
let qi = QualifiedIdentifier schema (cs proc)
exists <- doesProcExist schema proc
if exists
then do
let call = B.Stmt "select " V.empty True <>
asJson (callProc qi $ fromMaybe M.empty (decode reqBody))
body :: Maybe (Identity Text) <- H.maybeEx call
return $ responseLBS status200 [jsonH]
(cs $ fromMaybe "[]" $ runIdentity <$> body)
else return $ responseLBS status404 [] ""
-- check that proc exists
-- check that arg names are all specified
-- select * from "1".proc(a := "foo"::undefined) where whereT limit limitT
([table], "PUT") ->
handleJsonObj reqBody $ \obj -> do
let qt = qualify table
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 $ M.keys obj
if S.fromList tableCols == S.fromList cols
then do
let vals = M.elems obj
H.unitEx $ iffNotT
(whereT qt qq $ update qt cols vals)
(insertSelect qt cols vals)
return $ responseLBS status204 [ jsonH ] ""
else return $ if Prelude.null tableCols
then responseLBS status404 [] ""
else responseLBS status400 []
"You must specify all columns in PUT request"
([table], "PATCH") ->
handleJsonObj reqBody $ \obj -> do
let qt = qualify table
up = returningStarT
. whereT qt qq
$ update qt (map cs $ M.keys obj) (M.elems obj)
patch = withT up "t" $ B.Stmt
"select count(t), array_to_json(array_agg(row_to_json(t)))::character varying"
V.empty True
row <- H.maybeEx patch
let (queryTotal, body) =
fromMaybe (0 :: Int, Just "" :: Maybe Text) row
r = contentRangeH 0 (queryTotal-1) queryTotal
echoRequested = lookupHeader "Prefer" == Just "return=representation"
s = case () of _ | queryTotal == 0 -> status404
| echoRequested -> status200
| otherwise -> status204
return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else ""
([table], "DELETE") -> do
let qt = qualify table
del = countT
. returningStarT
. whereT qt qq
$ deleteFrom qt
row <- H.maybeEx del
let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row
return $ if deletedCount == 0
then responseLBS status404 [] ""
else responseLBS status204 [("Content-Range", "*/"<> cs (show deletedCount))] ""
(_, _) ->
return $ responseLBS status404 [] ""
where
path = pathInfo req
verb = requestMethod req
qq = queryString req
qualify = QualifiedIdentifier schema
hdrs = requestHeaders req
lookupHeader = flip lookup hdrs
accept = lookupHeader hAccept
schema = requestedSchema (cs $ configV1Schema conf) accept
authenticator = cs $ configDbUser conf
jwtSecret = cs $ configJwtSecret conf
range = rangeRequested hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
contentType = fromMaybe "application/json" $ contentTypeForAccept accept
contentTypeH = (hContentType, contentType)
sqlError :: t
sqlError = undefined
isSqlError :: t
isSqlError = undefined
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 :: Text -> Maybe BS.ByteString -> Text
requestedSchema v1schema accept =
case verStr of
Just [[_, ver]] -> if ver == "1" then v1schema else cs ver
_ -> v1schema
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: BS.ByteString
verStr = (=~ verRegex) <$> accept :: Maybe [[BS.ByteString]]
jsonMT :: BS.ByteString
jsonMT = "application/json"
csvMT :: BS.ByteString
csvMT = "text/csv"
allMT :: BS.ByteString
allMT = "*/*"
jsonH :: Header
jsonH = (hContentType, jsonMT)
contentTypeForAccept :: Maybe BS.ByteString -> Maybe BS.ByteString
contentTypeForAccept accept
| isNothing accept || has allMT || has jsonMT = Just jsonMT
| has csvMT = Just csvMT
| otherwise = Nothing
where
Just acceptH = accept
findInAccept = flip find $ parseHttpAccept acceptH
has = isJust . findInAccept . BS.isPrefixOf
bodyForAccept :: BS.ByteString -> QualifiedIdentifier -> StatementT
bodyForAccept contentType table
| contentType == csvMT = asCsvWithCount table
| otherwise = asJsonWithCount -- defaults to JSON
handleJsonObj :: BL.ByteString -> (Object -> H.Tx P.Postgres s Response)
-> H.Tx P.Postgres s Response
handleJsonObj reqBody handler = do
let p = eitherDecode reqBody
case p of
Left err ->
return $ responseLBS status400 [jsonH] jErr
where
jErr = encode . object $
[("message", String $ "Failed to parse JSON payload. " <> cs err)]
Right (Object o) -> handler o
Right _ ->
return $ responseLBS status400 [jsonH] jErr
where
jErr = encode . object $
[("message", String "Expecting a JSON object")]
parseCsvCell :: BL.ByteString -> Value
parseCsvCell s = if s == "NULL" then Null else String $ cs s
multipart :: Status -> [Response] -> Response
multipart _ [] = responseLBS status204 [] ""
multipart _ [r] = r
multipart s rs =
responseLBS s [(hContentType, "multipart/mixed; boundary=\"postgrest_boundary\"")] $
BL.intercalate "\n--postgrest_boundary\n" (map renderResponseBody rs)
where
renderHeader :: Header -> BL.ByteString
renderHeader (k, v) = cs (original k) <> ": " <> cs v
renderResponseBody :: Response -> BL.ByteString
renderResponseBody (ResponseBuilder _ headers b) =
BL.intercalate "\n" (map renderHeader headers)
<> "\n\n" <> BB.toLazyByteString b
renderResponseBody _ = error
"Unable to create multipart response from non-ResponseBuilder"
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [Text]
}
instance ToJSON TableOptions where
toJSON t = object [
"columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ]
+101
View File
@@ -0,0 +1,101 @@
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables, OverloadedStrings #-}
module PostgREST.Auth where
import Data.Aeson
import Control.Monad (mzero)
import Control.Applicative
import Crypto.BCrypt
import Data.Text
import Data.Monoid
import Data.Map
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import qualified Web.JWT as JWT
import Data.String.Conversions (cs)
import PostgREST.PgQuery (pgFmtLit)
import Prelude
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
type UserId = Text
data LoginAttempt =
NoCredentials
| MalformedAuth
| LoginFailed
| LoginSuccess DbRole UserId
deriving (Eq, Show)
checkPass :: Text -> Text -> Bool
checkPass = (. cs) . validatePassword . cs
setRole :: Text -> H.Tx P.Postgres s ()
setRole role = H.unitEx $ B.Stmt ("set local role " <> cs (pgFmtLit role)) V.empty True
setUserId :: Text -> H.Tx P.Postgres s ()
setUserId uid = if uid /= "" then
H.unitEx $ B.Stmt ("set local user_vars.user_id = " <> cs (pgFmtLit uid)) V.empty True
else
resetUserId
resetUserId :: H.Tx P.Postgres s ()
resetUserId = H.unitEx [H.stmt|reset user_vars.user_id|]
addUser :: Text -> Text -> Text -> H.Tx P.Postgres s ()
addUser identity pass role = do
let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
H.unitEx $
[H.stmt|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
identity (cs hashed :: Text) role
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
signInRole user pass = do
u <- H.maybeEx $ [H.stmt|select id, pass, rolname from postgrest.auth where id = ?|] user
return $ maybe LoginFailed (\r ->
let (uid, hashed, role) = r in
if checkPass hashed pass
then LoginSuccess role uid
else LoginFailed
) u
signInWithJWT :: Text -> Text -> LoginAttempt
signInWithJWT secret input = case maybeRole of
Just (Just (String role)) -> case maybeUserId of
Just (Just (String uid)) -> LoginSuccess (cs role) (cs uid)
_ -> LoginFailed
_ -> LoginFailed
where
maybeRole = (Data.Map.lookup "role" <$> claims) ::Maybe (Maybe Value)
maybeUserId = (Data.Map.lookup "id" <$> claims) ::Maybe (Maybe Value)
claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded
decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input
tokenJWT :: Text -> Text -> Text -> Text
tokenJWT secret uid role = JWT.encodeSigned JWT.HS256 (JWT.secret secret) claimsSet
where
claimsSet = JWT.def {
JWT.unregisteredClaims = Data.Map.fromList [("id", String uid), ("role", String role)]
}
+64
View File
@@ -0,0 +1,64 @@
module PostgREST.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(..))
import Prelude
data AppConfig = AppConfig {
configDbName :: String
, configDbPort :: Int
, configDbUser :: String
, configDbPass :: String
, configDbHost :: String
, configPort :: Int
, configAnonRole :: String
, configSecure :: Bool
, configPool :: Int
, configV1Schema :: String
, configJwtSecret :: String
}
argParser :: Parser AppConfig
argParser = AppConfig
<$> strOption (long "db-name" <> short 'd' <> metavar "NAME" <> help "name of database")
<*> option auto (long "db-port" <> short 'P' <> metavar "PORT" <> value 5432 <> help "postgres server port" <> showDefault)
<*> strOption (long "db-user" <> short 'U' <> metavar "ROLE" <> help "postgres authenticator role")
<*> strOption (long "db-pass" <> metavar "PASS" <> value "" <> help "password for authenticator role")
<*> strOption (long "db-host" <> metavar "HOST" <> value "localhost" <> help "postgres server hostname" <> showDefault)
<*> option auto (long "port" <> short 'p' <> metavar "PORT" <> value 3000 <> help "port number on which to run HTTP server" <> showDefault)
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE" <> help "postgres role to use for non-authenticated requests")
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
<*> option auto (long "db-pool" <> metavar "COUNT" <> value 10 <> help "Max connections in database pool" <> showDefault)
<*> strOption (long "v1schema" <> metavar "NAME" <> value "1" <> help "Schema to use for nonspecified version (or explicit v1)" <> showDefault)
<*> strOption (long "jwt-secret" <> metavar "SECRET" <> value "secret" <> help "Secret used to encrypt and decrypt JWT tokens)" <> showDefault)
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
(Just $ 60*60*24) False False True
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 -> []
+71
View File
@@ -0,0 +1,71 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
module PostgREST.Error (PgError, errResponse) where
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Network.HTTP.Types.Status as HT
import qualified Data.Aeson as JSON
import qualified Data.Text as T
import Data.Aeson ((.=))
import Data.String.Conversions (cs)
import Data.String.Utils(replace)
import Network.Wai(Response, responseLBS)
import Network.HTTP.Types.Header
type PgError = H.SessionError P.Postgres
errResponse :: PgError -> Response
errResponse e = responseLBS (httpStatus e)
[(hContentType, "application/json")] (JSON.encode e)
instance JSON.ToJSON PgError where
toJSON (H.TxError (P.ErroneousResult c m d h)) = JSON.object [
"code" .= (cs c::T.Text),
"message" .= (cs m::T.Text),
"details" .= (fmap cs d::Maybe T.Text),
"hint" .= (fmap cs h::Maybe T.Text)]
toJSON (H.TxError (P.NoResult d)) = JSON.object [
"message" .= ("No response from server"::T.Text),
"details" .= (fmap cs d::Maybe T.Text)]
toJSON (H.TxError (P.UnexpectedResult m)) = JSON.object ["message" .= m]
toJSON (H.TxError P.NotInTransaction) = JSON.object [
"message" .= ("Not in transaction"::T.Text)]
toJSON (H.CxError (P.CantConnect d)) = JSON.object [
"message" .= ("Can't connect to the database"::T.Text),
"details" .= (fmap cs d::Maybe T.Text)]
toJSON (H.CxError (P.UnsupportedVersion v)) = JSON.object [
"message" .= ("Postgres version "++version++" is not supported") ]
where version = replace "0" "." (show v)
toJSON (H.ResultError m) = JSON.object ["message" .= m]
httpStatus :: PgError -> HT.Status
httpStatus (H.TxError (P.ErroneousResult codeBS _ _ _)) =
let code = cs codeBS in
case code of
'0':'8':_ -> HT.status503 -- pg connection err
'0':'9':_ -> HT.status500 -- triggered action exception
'0':'L':_ -> HT.status403 -- invalid grantor
'0':'P':_ -> HT.status403 -- invalid role specification
'2':'5':_ -> HT.status500 -- invalid tx state
'2':'8':_ -> HT.status403 -- invalid auth specification
'2':'D':_ -> HT.status500 -- invalid tx termination
'3':'8':_ -> HT.status500 -- external routine exception
'3':'9':_ -> HT.status500 -- external routine invocation
'3':'B':_ -> HT.status500 -- savepoint exception
'4':'0':_ -> HT.status500 -- tx rollback
'5':'3':_ -> HT.status503 -- insufficient resources
'5':'4':_ -> HT.status413 -- too complex
'5':'5':_ -> HT.status500 -- obj not on prereq state
'5':'7':_ -> HT.status500 -- operator intervention
'5':'8':_ -> HT.status500 -- system error
'F':'0':_ -> HT.status500 -- conf file error
'H':'V':_ -> HT.status500 -- foreign data wrapper error
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
'X':'X':_ -> HT.status500 -- internal Error
"42P01" -> HT.status404 -- undefined table
"42501" -> HT.status404 -- insufficient privilege
_ -> HT.status400
httpStatus (H.TxError (P.NoResult _)) = HT.status503
httpStatus _ = HT.status500
+81
View File
@@ -0,0 +1,81 @@
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables #-}
module Main where
import Paths_postgrest (version)
import PostgREST.App
import PostgREST.Middleware
import PostgREST.Error(errResponse)
import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO)
import Data.String.Conversions (cs)
import Network.Wai (strictRequestBody)
import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.RequestLogger (logStdout)
import Data.List (intercalate)
import Data.Version (versionBranch)
import Data.Functor.Identity
import Data.Text(Text)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Options.Applicative hiding (columns)
import System.IO (stderr, stdin, stdout, hSetBuffering, BufferMode(..))
import PostgREST.Config (AppConfig(..), argParser)
isServerVersionSupported = do
Identity (row :: Text) <- H.tx Nothing $ H.singleEx $ [H.stmt|SHOW server_version_num|]
return $ read (cs row) >= 90200
main :: IO ()
main = do
hSetBuffering stdout LineBuffering
hSetBuffering stdin LineBuffering
hSetBuffering stderr NoBuffering
let opts = info (helper <*> argParser) $
fullDesc
<> progDesc (
"PostgREST "
<> prettyVersion
<> " / create a REST API to an existing Postgres database"
)
parserPrefs = prefs showHelpOnError
conf <- customExecParser parserPrefs opts
let port = configPort conf
unless (configSecure conf) $
putStrLn "WARNING, running in insecure mode, auth will be in plaintext"
unless ("secret" /= configJwtSecret conf) $
putStrLn "WARNING, running in insecure mode, JWT secret is the default value"
Prelude.putStrLn $ "Listening on port " ++
(show $ configPort conf :: String)
let pgSettings = P.ParamSettings (cs $ configDbHost conf)
(fromIntegral $ configDbPort conf)
(cs $ configDbUser conf)
(cs $ configDbPass conf)
(cs $ configDbName conf)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle = logStdout . defaultMiddle (configSecure conf)
poolSettings <- maybe (fail "Improper session settings") return $
H.poolSettings (fromIntegral $ configPool conf) 30
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings poolSettings
resOrError <- H.session pool isServerVersionSupported
either (fail . show) (\supported -> unless supported $ fail "Cannot run in this PostgreSQL version, PostgREST needs at least 9.2.0") resOrError
runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx (Just (H.ReadCommitted, Just True)) $
authenticated conf (app conf body) req
either (respond . errResponse) respond resOrError
where
prettyVersion = intercalate "." $ map show $ versionBranch version
+101
View File
@@ -0,0 +1,101 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
module PostgREST.Middleware where
import Data.Maybe (fromMaybe, isNothing)
import Data.Monoid
import Data.Text
-- import Data.Pool(withResource, Pool)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Data.String.Conversions(cs)
import Network.HTTP.Types.Header (hLocation, hAuthorization, hAccept)
import Network.HTTP.Types (RequestHeaders)
import Network.HTTP.Types.Status (status400, status401, status301, status415)
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
rawQueryString, isSecure, Request(..), Response)
import Network.Wai.Middleware.Gzip (gzip, def)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Static (staticPolicy, only)
import Network.URI (URI(..), parseURI)
import PostgREST.Config (AppConfig(..), corsPolicy)
import PostgREST.Auth (LoginAttempt(..), signInRole, signInWithJWT, setRole, setUserId)
import PostgREST.App (contentTypeForAccept)
import Codec.Binary.Base64.String (decode)
import Prelude
authenticated :: forall s. AppConfig ->
(Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response
authenticated conf app req = do
attempt <- httpRequesterRole (requestHeaders req)
case attempt of
MalformedAuth ->
return $ responseLBS status400 [] "Malformed basic auth header"
LoginFailed ->
return $ responseLBS status401 [] "Invalid username or password"
LoginSuccess role uid -> if role /= currentRole then runInRole role uid else app req
NoCredentials -> if anon /= currentRole then runInRole anon "" else app req
where
jwtSecret = cs $ configJwtSecret conf
currentRole = cs $ configDbUser conf
anon = cs $ configAnonRole conf
httpRequesterRole :: RequestHeaders -> H.Tx P.Postgres s LoginAttempt
httpRequesterRole hdrs = do
let auth = fromMaybe "" $ lookup hAuthorization hdrs
case split (==' ') (cs auth) of
("Basic" : b64 : _) ->
case split (==':') (cs . decode . cs $ b64) of
(u:p:_) -> signInRole u p
_ -> return MalformedAuth
("Bearer" : jwt : _) ->
return $ signInWithJWT jwtSecret jwt
_ -> return NoCredentials
runInRole :: Text -> Text -> H.Tx P.Postgres s Response
runInRole r uid = do
setUserId uid
setRole r
app req
redirectInsecure :: Application -> Application
redirectInsecure app req respond = do
let hdrs = requestHeaders req
host = lookup "host" hdrs
uriM = parseURI . cs =<< mconcat [
Just "https://",
host,
Just $ rawPathInfo req,
Just $ rawQueryString req]
isHerokuSecure = lookup "x-forwarded-proto" hdrs == Just "https"
if not (isSecure req || isHerokuSecure)
then case uriM of
Just uri ->
respond $ responseLBS status301 [
(hLocation, cs . show $ uri { uriScheme = "https:" })
] ""
Nothing ->
respond $ responseLBS status400 [] "SSL is required"
else app req respond
unsupportedAccept :: Application -> Application
unsupportedAccept app req respond = do
let
accept = lookup hAccept $ requestHeaders req
if isNothing $ contentTypeForAccept accept
then respond $ responseLBS status415 [] "Unsupported Accept header, try: application/json"
else app req respond
defaultMiddle :: Bool -> Application -> Application
defaultMiddle secure = (if secure then redirectInsecure else id)
. gzip def . cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
. unsupportedAccept
+322
View File
@@ -0,0 +1,322 @@
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, MultiWayIf #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module PostgREST.PgQuery where
import PostgREST.RangeQuery
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Hasql.Backend as B
import qualified Data.Text as T
import qualified Data.HashMap.Strict as H
import Text.Regex.TDFA ( (=~) )
import qualified Network.HTTP.Types.URI as Net
import qualified Data.ByteString.Char8 as BS
import Data.Monoid
import Data.Vector (empty)
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 qualified Data.Vector as V
import Data.Scientific (isInteger, formatScientific, FPFormat(..))
import Prelude
type PStmt = H.Stmt P.Postgres
instance Monoid PStmt where
mappend (B.Stmt query params prep) (B.Stmt query' params' prep') =
B.Stmt (query <> query') (params <> params') (prep && prep')
mempty = B.Stmt "" empty True
type StatementT = PStmt -> PStmt
data QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: T.Text
, qiName :: T.Text
} deriving (Show)
data OrderTerm = OrderTerm {
otTerm :: T.Text
, otDirection :: BS.ByteString
, otNullOrder :: Maybe BS.ByteString
}
limitT :: Maybe NonnegRange -> StatementT
limitT r q =
q <> B.Stmt (" LIMIT " <> limit <> " OFFSET " <> offset <> " ") empty True
where
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
whereT :: QualifiedIdentifier -> Net.Query -> StatementT
whereT table params q =
if L.null cols
then q
else q <> B.Stmt " where " empty True <> conjunction
where
cols = [ col | col <- params, fst col `notElem` ["order"] ]
wherePredTable = wherePred table
conjunction = mconcat $ L.intersperse andq (map wherePredTable cols)
withT :: PStmt -> T.Text -> StatementT
withT (B.Stmt eq ep epre) v (B.Stmt wq wp wpre) =
B.Stmt ("WITH " <> v <> " AS (" <> eq <> ") " <> wq <> " from " <> v)
(ep <> wp)
(epre && wpre)
orderT :: [OrderTerm] -> StatementT
orderT ts q =
if L.null ts
then q
else q <> B.Stmt " order by " empty True <> clause
where
clause = mconcat $ L.intersperse commaq (map queryTerm ts)
queryTerm :: OrderTerm -> PStmt
queryTerm t = B.Stmt
(" " <> cs (pgFmtIdent $ otTerm t) <> " "
<> cs (otDirection t) <> " "
<> maybe "" cs (otNullOrder t) <> " ")
empty True
parentheticT :: StatementT
parentheticT s =
s { B.stmtTemplate = " (" <> B.stmtTemplate s <> ") " }
iffNotT :: PStmt -> StatementT
iffNotT (B.Stmt aq ap apre) (B.Stmt bq bp bpre) =
B.Stmt
("WITH aaa AS (" <> aq <> " returning *) " <>
bq <> " WHERE NOT EXISTS (SELECT * FROM aaa)")
(ap <> bp)
(apre && bpre)
countT :: StatementT
countT s =
s { B.stmtTemplate = "WITH qqq AS (" <> B.stmtTemplate s <> ") SELECT pg_catalog.count(1) FROM qqq" }
countRows :: QualifiedIdentifier -> PStmt
countRows t = B.Stmt ("select pg_catalog.count(1) from " <> fromQi t) empty True
asCsvWithCount :: QualifiedIdentifier -> StatementT
asCsvWithCount table = withCount . asCsv table
asCsv :: QualifiedIdentifier -> StatementT
asCsv table s = s { B.stmtTemplate =
"(select string_agg(quote_ident(column_name::text), ',') from "
<> "(select column_name from information_schema.columns where quote_ident(table_schema) || '.' || table_name = '"
<> fromQi table <> "' order by ordinal_position) h) || '\r' || "
<> "coalesce(string_agg(substring(t::text, 2, length(t::text) - 2), '\r'), '') from ("
<> B.stmtTemplate s <> ") t" }
asJsonWithCount :: StatementT
asJsonWithCount = withCount . asJson
asJson :: StatementT
asJson s = s { B.stmtTemplate =
"array_to_json(array_agg(row_to_json(t)))::character varying from ("
<> B.stmtTemplate s <> ") t" }
withCount :: StatementT
withCount s = s { B.stmtTemplate = "pg_catalog.count(t), " <> B.stmtTemplate s }
asJsonRow :: StatementT
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
selectStar :: QualifiedIdentifier -> PStmt
selectStar t = B.Stmt ("select * from " <> fromQi t) empty True
returningStarT :: StatementT
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
deleteFrom :: QualifiedIdentifier -> PStmt
deleteFrom t = B.Stmt ("delete from " <> fromQi t) empty True
insertInto :: QualifiedIdentifier
-> V.Vector T.Text
-> V.Vector (V.Vector JSON.Value)
-> PStmt
insertInto t cols vals
| V.null cols = B.Stmt ("insert into " <> fromQi t <> " default values returning *") empty True
| otherwise = B.Stmt
("insert into " <> fromQi t <> " (" <>
T.intercalate ", " (V.toList $ V.map pgFmtIdent cols) <>
") values "
<> T.intercalate ", "
(V.toList $ V.map (\v -> "("
<> T.intercalate ", " (V.toList $ V.map insertableValue v)
<> ")"
) vals
)
<> " returning row_to_json(" <> fromQi t <> ".*)")
empty True
insertSelect :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
insertSelect t [] _ = B.Stmt
("insert into " <> fromQi t <> " default values returning *") empty True
insertSelect t cols vals = B.Stmt
("insert into " <> fromQi t <> " ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") select "
<> T.intercalate ", " (map insertableValue vals))
empty True
update :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
update t cols vals = B.Stmt
("update " <> fromQi t <> " set ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") = ("
<> T.intercalate ", " (map insertableValue vals)
<> ")")
empty True
callProc :: QualifiedIdentifier -> JSON.Object -> PStmt
callProc qi params = do
let args = T.intercalate "," $ map assignment (H.toList params)
B.Stmt ("select * from " <> fromQi qi <> "(" <> args <> ")") empty True
where
assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
wherePred :: QualifiedIdentifier -> Net.QueryItem -> PStmt
wherePred table (col, predicate) =
B.Stmt (notOp <> " " <> pgFmtJsonbPath table (cs col) <> " " <> op <> " " <>
if opCode `elem` ["is","isnot"] then whiteList value
else cs sqlValue)
empty True
where
headPredicate:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse
opCode = hasNot (head rest) headPredicate
notOp = hasNot headPredicate ""
value = hasNot (T.intercalate "." $ tail rest) (T.intercalate "." rest)
whiteList val = fromMaybe (cs (pgFmtLit val) <> "::unknown ")
(L.find ((==) . T.toLower $ val)
["null","true","false"])
star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit
sqlValue = case opCode of
"like" -> unknownLiteral $ T.map star value
"ilike" -> unknownLiteral $ T.map star value
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
_ -> unknownLiteral value
op = case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
"like"-> "like"
"ilike"-> "ilike"
"in" -> "in"
"notin" -> "not in"
"is" -> "is"
"isnot" -> "is not"
"@@" -> "@@"
_ -> "="
orderParse :: Net.Query -> [OrderTerm]
orderParse q =
mapMaybe orderParseTerm . T.split (==',') $ cs order
where
order = fromMaybe "" $ join (lookup "order" q)
orderParseTerm :: T.Text -> Maybe OrderTerm
orderParseTerm s =
case T.split (=='.') s of
(c:d:nls) ->
if d `elem` ["asc", "desc"]
then Just $ OrderTerm c
( if d == "asc" then "asc" else "desc" )
( case nls of
[n] -> if | n == "nullsfirst" -> Just "nulls first"
| n == "nullslast" -> Just "nulls last"
| otherwise -> Nothing
_ -> Nothing
)
else Nothing
_ -> Nothing
commaq :: PStmt
commaq = B.Stmt ", " empty True
andq :: PStmt
andq = B.Stmt " and " empty True
data JsonbPath =
ColIdentifier T.Text
| KeyIdentifier T.Text
| SingleArrow JsonbPath JsonbPath
| DoubleArrow JsonbPath JsonbPath
deriving (Show)
parseJsonbPath :: T.Text -> Maybe JsonbPath
parseJsonbPath p =
case T.splitOn "->>" p of
[a,b] ->
let i:is = T.splitOn "->" a in
Just $ DoubleArrow
(foldl SingleArrow (ColIdentifier i) (map KeyIdentifier is))
(KeyIdentifier b)
_ -> Nothing
pgFmtJsonbPath :: QualifiedIdentifier -> T.Text -> T.Text
pgFmtJsonbPath table p =
pgFmtJsonbPath' $ fromMaybe (ColIdentifier p) (parseJsonbPath p)
where
pgFmtJsonbPath' (ColIdentifier i) = fromQi table <> "." <> pgFmtIdent i
pgFmtJsonbPath' (KeyIdentifier i) = pgFmtLit i
pgFmtJsonbPath' (SingleArrow a b) =
pgFmtJsonbPath' a <> "->" <> pgFmtJsonbPath' b
pgFmtJsonbPath' (DoubleArrow a b) =
pgFmtJsonbPath' a <> "->>" <> pgFmtJsonbPath' b
pgFmtIdent :: T.Text -> T.Text
pgFmtIdent x =
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
if (cs escaped :: BS.ByteString) =~ danger
then "\"" <> escaped <> "\""
else escaped
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: BS.ByteString
pgFmtLit :: T.Text -> T.Text
pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
slashed = T.replace "\\" "\\\\" escaped in
if T.isInfixOf "\\\\" escaped
then "E" <> slashed
else slashed
trimNullChars :: T.Text -> T.Text
trimNullChars = T.takeWhile (/= '\x0')
fromQi :: QualifiedIdentifier -> T.Text
fromQi t = pgFmtIdent (qiSchema t) <> "." <> pgFmtIdent (qiName t)
unquoted :: JSON.Value -> T.Text
unquoted (JSON.String t) = t
unquoted (JSON.Number n) =
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
unquoted (JSON.Bool b) = cs . show $ b
unquoted v = cs $ JSON.encode v
insertableText :: T.Text -> T.Text
insertableText = (<> "::unknown") . pgFmtLit
insertableValue :: JSON.Value -> T.Text
insertableValue JSON.Null = "null"
insertableValue v = insertableText $ unquoted v
paramFilter :: JSON.Value -> T.Text
paramFilter JSON.Null = "is.null"
paramFilter v = "eq." <> unquoted v
+198
View File
@@ -0,0 +1,198 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings, TypeSynonymInstances,
MultiParamTypeClasses, ScopedTypeVariables,
FlexibleContexts #-}
module PostgREST.PgStructure where
import PostgREST.PgQuery (QualifiedIdentifier(..))
import Data.Text hiding (foldl, map, zipWith, concat)
import Data.Aeson
import Data.Functor.Identity
import Data.String.Conversions (cs)
import Data.Maybe (fromMaybe, isJust)
import Control.Applicative
import qualified Data.Map as Map
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Prelude
foreignKeys :: QualifiedIdentifier -> H.Tx P.Postgres s (Map.Map Text ForeignKey)
foreignKeys table = do
r <- H.listEx $ [H.stmt|
select kcu.column_name, ccu.table_name AS foreign_table_name,
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
|] (qiName table) (qiSchema table)
return $ foldl addKey Map.empty r
where
addKey :: Map.Map Text ForeignKey -> (Text, Text, Text) -> Map.Map Text ForeignKey
addKey m (col, ftab, fcol) = Map.insert col (ForeignKey ftab fcol) m
tables :: Text -> H.Tx P.Postgres s [Table]
tables schema = do
rows <- H.listEx $
[H.stmt|
select
n.nspname as table_schema,
relname as table_name,
c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8
or (exists (
select 1
from pg_trigger
where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69)
) as insertable
from
pg_class c
join pg_namespace n on n.oid = c.relnamespace
where
c.relkind in ('v', 'r', 'm')
and n.nspname = ?
and (
pg_has_role(c.relowner, 'USAGE'::text)
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text)
)
order by relname
|] schema
return $ map tableFromRow rows
columns :: QualifiedIdentifier -> H.Tx P.Postgres s [Column]
columns table = do
cols <- H.listEx $ [H.stmt|
select info.table_schema as schema, info.table_name as table_name,
info.column_name as name, info.ordinal_position as position,
info.is_nullable::boolean as nullable, info.data_type as col_type,
info.is_updatable::boolean as updatable,
info.character_maximum_length as max_len,
info.numeric_precision as precision,
info.column_default as default_value,
array_to_string(enum_info.vals, ',') as enum
from (
select table_schema, table_name, column_name, ordinal_position,
is_nullable, data_type, is_updatable,
character_maximum_length, numeric_precision,
column_default, udt_name
from information_schema.columns
where table_schema = ? 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 |]
(qiSchema table) (qiName table)
fks <- foreignKeys table
return $ map (addFK fks . columnFromRow) cols
where
addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks }
primaryKeyColumns :: QualifiedIdentifier -> H.Tx P.Postgres s [Text]
primaryKeyColumns table = do
r <- H.listEx $ [H.stmt|
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 = ? |] (qiSchema table) (qiName table)
return $ map runIdentity r
doesProcExist :: Text -> Text -> H.Tx P.Postgres s Bool
doesProcExist schema proc = do
row :: Maybe (Identity Int) <- H.maybeEx $ [H.stmt|
SELECT 1
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p
ON pronamespace = n.oid
WHERE nspname = ?
AND proname = ?
|] schema proc
return $ isJust row
data Table = Table {
tableSchema :: Text
, 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)
tableFromRow :: (Text, Text, Bool) -> Table
tableFromRow (s, n, i) = Table s n i
columnFromRow :: (Text, Text, Text,
Int, Bool, Text,
Bool, Maybe Int, Maybe Int,
Maybe Text, Maybe Text)
-> Column
columnFromRow (s, t, n, pos, nul, typ, u, l, p, d, e) =
Column s t n pos nul typ u l p d (parseEnum e) Nothing
where
parseEnum :: Maybe Text -> [Text]
parseEnum str = fromMaybe [] $ split (==',') <$> str
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 ]
@@ -1,10 +1,16 @@
{-# LANGUAGE OverloadedStrings #-}
module RangeQuery where
module PostgREST.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
@@ -14,8 +20,37 @@ import Text.Read (readMaybe)
import Data.Maybe (fromMaybe, listToMaybe)
import Prelude
type NonnegRange = Range Int
rangeParse :: BS.ByteString -> Maybe NonnegRange
rangeParse range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
parsedRange <- listToMaybe (range =~ rangeRegex :: [[BS.ByteString]])
let [_, from, to] = readMaybe . cs <$> parsedRange
let lower = fromMaybe emptyRange (rangeGeq <$> from)
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to)
return $ rangeIntersection lower upper
rangeRequested :: RequestHeaders -> Maybe NonnegRange
rangeRequested = (rangeParse =<<) . lookup hRange
rangeLimit :: NonnegRange -> Maybe Int
rangeLimit range =
case [rangeLower range, rangeUpper range]
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
rangeOffset :: NonnegRange -> Int
rangeOffset range =
case rangeLower range
of BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen
rangeGeq :: Int -> NonnegRange
rangeGeq n =
Range (BoundaryBelow n) BoundaryAboveAll
@@ -23,33 +58,3 @@ rangeGeq n =
rangeLeq :: Int -> NonnegRange
rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n)
parseRange :: String -> Maybe NonnegRange
parseRange range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: String
parsedRange <- listToMaybe (range =~ rangeRegex :: [[String]])
let [_, from, to] = readMaybe <$> parsedRange
let lower = fromMaybe emptyRange (rangeGeq <$> from)
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to)
return $ rangeIntersection lower upper
requestedRange :: RequestHeaders -> Maybe NonnegRange
requestedRange hdrs = parseRange =<< cs <$> lookup hRange hdrs
requestedContentRange :: RequestHeaders -> Maybe NonnegRange
requestedContentRange hdrs = parseRange =<< cs <$> lookup "Content-Range" hdrs
limit :: NonnegRange -> Maybe Int
limit range =
case [rangeLower range, rangeUpper range]
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
offset :: NonnegRange -> Int
offset range =
case rangeLower range
of BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen
-61
View File
@@ -1,61 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Types where
import Database.HDBC (toSql, iToSql, SqlValue(..))
import qualified Data.Aeson as JSON
import Data.Aeson.Types (Parser)
import Data.Scientific (floatingOrInteger)
import Data.HashMap.Strict (foldlWithKey')
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Calendar (showGregorian)
import Control.Monad (mzero)
instance JSON.FromJSON SqlValue where
parseJSON (JSON.Number n) = return $ either toSql iToSql (floatingOrInteger n :: Either Double Int)
parseJSON (JSON.String s) = return $ toSql s
parseJSON (JSON.Bool b) = return $ toSql b
parseJSON JSON.Null = return SqlNull
parseJSON (JSON.Object o) = return . toSql $ JSON.encode o
parseJSON (JSON.Array a) = return . toSql $ JSON.encode a
instance JSON.ToJSON SqlValue where
toJSON (SqlString s) = JSON.toJSON s
toJSON (SqlByteString s) = JSON.toJSON $ decodeUtf8 s
toJSON (SqlWord32 w) = JSON.toJSON w
toJSON (SqlWord64 w) = JSON.toJSON w
toJSON (SqlInt32 i) = JSON.toJSON i
toJSON (SqlInt64 i) = JSON.toJSON i
toJSON (SqlInteger i) = JSON.toJSON i
toJSON (SqlChar c) = JSON.toJSON c
toJSON (SqlBool b) = JSON.toJSON b
toJSON (SqlDouble n) = JSON.toJSON n
toJSON (SqlRational n) = JSON.toJSON n
toJSON (SqlLocalDate d) = JSON.toJSON $ showGregorian d
toJSON (SqlLocalTimeOfDay t) = JSON.toJSON $ show t
toJSON (SqlLocalTime t) = JSON.toJSON $ show t
toJSON SqlNull = JSON.Null
toJSON x = JSON.toJSON $ show x
newtype SqlRow = SqlRow {getRow :: [(Text, SqlValue)] } deriving (Show)
sqlRowColumns :: SqlRow -> [Text]
sqlRowColumns = map fst . getRow
sqlRowValues :: SqlRow -> [SqlValue]
sqlRowValues = map snd . getRow
instance JSON.FromJSON SqlRow where
parseJSON (JSON.Object m) = foldlWithKey' add (return $ SqlRow []) m
where
add :: Parser SqlRow -> Text -> JSON.Value -> Parser SqlRow
add parser k v = do
SqlRow l <- parser
sqlV <- JSON.parseJSON v
return . SqlRow $ (k, sqlV) : l
parseJSON _ = mzero
+7
View File
@@ -0,0 +1,7 @@
flags: {}
packages:
- '.'
extra-deps:
- Ranged-sets-0.3.0
- packdeps-0.4.1
resolver: lts-3.1
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+50 -16
View File
@@ -1,27 +1,61 @@
{-# LANGUAGE OverloadedStrings #-}
module Feature.AuthSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import SpecHelper
-- }}}
spec :: Spec
spec = around appWithFixture $
describe "authorization" $ do
it "hides tables that anonymous does not own" $ do
pendingWith_ "Fix pg exception"
get "/authors_only" `shouldRespondWith` 400 -- TODO: should be 404
it "indicates login failure" $ do
pendingWith_ "Fix pg exception"
let auth = authHeader "dbapi_test_author_a" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "allows users with permissions to see their tables" $ do
pendingWith_ "Fix pg exception"
let auth = authHeader "dbapi_test_author_a" ""
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
spec = beforeAll
(clearTable "postgrest.auth") . afterAll_ (clearTable "postgrest.auth")
$ around withApp
$ describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
it "indicates login failure (BasicAuth)" $ do
let auth = authHeaderBasic "postgrest_test_author" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "allows users with permissions to see their tables (BasicAuth)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "recovers after 400 error with logged in user" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
_ <- request methodPost "/rpc/problem" [auth] ""
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "allows users to login (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
post "/postgrest/tokens" [json| { "id":"jdoe", "pass": "1234" } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json"]
}
it "indicates login failure (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
post "/postgrest/tokens" [json| { "id":"jdoe", "pass": "NOPE" } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"message":"Failed authentication."} |]
, matchStatus = 401
, matchHeaders = ["Content-Type" <:> "application/json"]
}
it "allows users with permissions to see their tables (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
+10 -5
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE OverloadedStrings #-}
module Feature.CorsSpec where
-- {{{ Imports
@@ -14,8 +12,7 @@ import Network.HTTP.Types
-- }}}
spec :: Spec
spec = around appWithFixture $
describe "CORS" $ do
spec = around withApp $ describe "CORS" $ do
let preflightHeaders = [
("Accept", "*/*"),
("Origin", "http://example.com"),
@@ -25,7 +22,7 @@ spec = around appWithFixture $
("Host", "localhost:3000"),
("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"),
("Origin", "http://localhost:8000"),
("Accept", "text/plain, */*; q=0.01"),
("Accept", "text/csv, */*; q=0.01"),
("Accept-Language", "en-US,en;q=0.5"),
("Accept-Encoding", "gzip, deflate"),
("Referer", "http://localhost:8000/"),
@@ -56,6 +53,14 @@ spec = around appWithFixture $
r <- request methodOptions "/" preflightHeaders ""
liftIO $ simpleBody r `shouldBe` ""
describe "regular request" $
it "exposes necesssary response headers" $ do
r <- request methodGet "/items" [("Origin", "http://example.com")] ""
liftIO $ simpleHeaders r `shouldSatisfy` matchHeader
"Access-Control-Expose-Headers"
"Content-Encoding, Content-Location, Content-Range, Content-Type, \
\Date, Location, Server, Transfer-Encoding, Range-Unit"
describe "postflight request" $
it "allows INFO body through even with CORS request headers present" $ do
r <- request methodOptions "/items" normalCors ""
+37
View File
@@ -0,0 +1,37 @@
module Feature.DeleteSpec where
import Test.Hspec
import Test.Hspec.Wai
import SpecHelper
import Network.HTTP.Types
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
describe "Deleting" $ do
context "existing record" $ do
it "succeeds with 204 and deletion count" $
request methodDelete "/items?id=eq.1" [] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 204
, matchHeaders = ["Content-Range" <:> "*/1"]
}
it "actually clears items ouf the db" $ do
_ <- request methodDelete "/items?id=lt.15" [] ""
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":15}]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
context "known route, unknown record" $
it "fails with 404" $
request methodDelete "/items?id=eq.101" [] "" `shouldRespondWith` 404
context "totally unknown route" $
it "fails with 404" $
request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404
+205 -32
View File
@@ -1,8 +1,5 @@
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
module Feature.InsertSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
@@ -12,34 +9,36 @@ import SpecHelper
import qualified Data.Aeson as JSON
import Data.Maybe (fromJust)
import Text.Heredoc
import Network.HTTP.Types.Header
import Network.HTTP.Types
import Control.Monad (replicateM_)
import TestTypes(IncPK, incStr, incNullableStr)
-- }}}
import TestTypes(IncPK(..), CompoundPK(..))
spec :: Spec
spec = around appWithFixture $ do
spec = afterAll_ resetDb $ around withApp $ do
describe "Posting new record" $ do
it "accepts disparate json types" $
post "/menagerie"
after_ (clearTable "menagerie") . it "accepts disparate json types" $ do
p <- post "/menagerie"
[json| {
"integer": 13, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "01/01/1900", "money": "$3.99"
, "enum": ["foo"]
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
} |]
`shouldRespondWith` 201
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
context "with no pk supplied" $ do
context "into a table with auto-incrementing pk" $
context "into a table with auto-incrementing pk" . after_ (clearTable "auto_incrementing_pk") $
it "succeeds with 201 and link" $ do
p <- post "/auto_incrementing_pk" [json| { "non_nullable_string":"not null"} |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/auto_incrementing_pk\\?id=eq\\.[0-9]+"
simpleStatus p `shouldBe` created201
let Just location = getHeader hLocation $ simpleHeaders p
let Just location = lookup hLocation $ simpleHeaders p
r <- get location
let [record] = fromJust (JSON.decode $ simpleBody r :: Maybe [IncPK])
liftIO $ do
@@ -47,12 +46,11 @@ spec = around appWithFixture $ do
incNullableStr record `shouldBe` Nothing
context "into a table with simple pk" $
it "fails with 400 and error" $ do
pendingWith_ "Fix pg exception"
it "fails with 400 and error" $
post "/simple_pk" [json| { "extra":"foo"} |]
`shouldRespondWith` 400
context "into a table with no pk" $
context "into a table with no pk" . after_ (clearTable "no_pk") $ do
it "succeeds with 201 and a link including all fields" $ do
p <- post "/no_pk" [json| { "a":"foo", "b":"bar" } |]
liftIO $ do
@@ -60,7 +58,25 @@ spec = around appWithFixture $ do
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
simpleStatus p `shouldBe` created201
context "with compound pk supplied" $
it "returns full details of inserted record if asked" $ do
p <- request methodPost "/no_pk"
[("Prefer", "return=representation")]
[json| { "a":"bar", "b":"baz" } |]
liftIO $ do
simpleBody p `shouldBe` [json| { "a":"bar", "b":"baz" } |]
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.bar&b=eq.baz"
simpleStatus p `shouldBe` created201
it "can post nulls" $ do
p <- request methodPost "/no_pk"
[("Prefer", "return=representation")]
[json| { "a":null, "b":"foo" } |]
liftIO $ do
simpleBody p `shouldBe` [json| { "a":null, "b":"foo" } |]
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=is.null&b=eq.foo"
simpleStatus p `shouldBe` created201
context "with compound pk supplied" . after_ (clearTable "compound_pk") $
it "builds response location header appropriately" $
post "/compound_pk" [json| { "k1":12, "k2":42 } |]
`shouldRespondWith` ResponseMatcher {
@@ -69,6 +85,70 @@ spec = around appWithFixture $ do
matchHeaders = ["Location" <:> "/compound_pk?k1=eq.12&k2=eq.42"]
}
context "with invalid json payload" $
it "fails with 400 and error" $
post "/simple_pk" "}{ x = 2" `shouldRespondWith` 400
context "jsonb" . after_ (clearTable "json") $ do
it "serializes nested object" $ do
let inserted = [json| { "data": { "foo":"bar" } } |]
p <- request methodPost "json" [("Prefer", "return=representation")] inserted
liftIO $ do
simpleBody p `shouldBe` inserted
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%7B%22foo%22%3A%22bar%22%7D"
simpleStatus p `shouldBe` created201
it "serializes nested array" $ do
let inserted = [json| { "data": [1,2,3] } |]
p <- request methodPost "json" [("Prefer", "return=representation")] inserted
liftIO $ do
simpleBody p `shouldBe` inserted
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%5B1%2C2%2C3%5D"
simpleStatus p `shouldBe` created201
describe "CSV insert" $ do
after_ (clearTable "menagerie") . context "disparate csv types" $
it "succeeds with multipart response" $ do
p <- request methodPost "/menagerie" [("Content-Type", "text/csv")]
[str|integer,double,varchar,boolean,date,money,enum
|13,3.14159,testing!,false,1900-01-01,$3.99,foo
|12,0.1,a string,true,1929-10-01,12,bar
|]
liftIO $ do
simpleBody p `shouldBe` "Content-Type: application/json\nLocation: /menagerie?integer=eq.13\n\n\n--postgrest_boundary\nContent-Type: application/json\nLocation: /menagerie?integer=eq.12\n\n"
simpleStatus p `shouldBe` created201
after_ (clearTable "no_pk") . context "requesting full representation" $ do
it "returns full details of inserted record" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Prefer", "return=representation")]
"a,b\nbar,baz"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| { "a":"bar", "b":"baz" } |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json",
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
}
it "can post nulls" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Prefer", "return=representation")]
"a,b\nNULL,foo"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| { "a":null, "b":"foo" } |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json",
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
}
after_ (clearTable "no_pk") . context "with wrong number of columns" $ do
it "fails for too few" $ do
p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"
liftIO $ simpleStatus p `shouldBe` badRequest400
it "fails for too many" $ do
p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz,bat,bad"
liftIO $ simpleStatus p `shouldBe` badRequest400
describe "Putting record" $ do
context "to unkonwn uri" $
@@ -86,39 +166,132 @@ spec = around appWithFixture $ do
context "with a fully-specified primary key" $ do
context "with Content-Range header" $
it "fails as per RFC7231" $
request methodPut "/compound_pk?k1=eq.1&k2=eq.2"
[("Content-Range", "0-0")]
[json| { "k1":1, "k2":2, "extra":3 } |]
`shouldRespondWith` 400
context "not specifying every column in the table" $
it "is rejected for lack of idempotence" $
request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 400
context "specifying every column in the table" $
it "succeeds with 201 and link" $ do
context "specifying every column in the table" . after_ (clearTable "compound_pk") $ do
it "can create a new record" $ do
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":3 } |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` status200
simpleStatus p `shouldBe` status204
context "with an auto-incrementing primary key" $
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
liftIO $ do
length rows `shouldBe` 1
let record = head rows
compoundK1 record `shouldBe` 12
compoundK2 record `shouldBe` 42
compoundExtra record `shouldBe` Just 3
it "succeeds with 201 and link" $
it "can update an existing record" $ do
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":4 } |]
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":5 } |]
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
liftIO $ do
length rows `shouldBe` 1
let record = head rows
compoundExtra record `shouldBe` Just 5
context "with an auto-incrementing primary key" . after_ (clearTable "auto_incrementing_pk") $
it "succeeds with 204" $
request methodPut "/auto_incrementing_pk?id=eq.1" []
[json| {
"id":1,
"nullable_string":"hi",
"non_nullable_string":"bye",
"inserted_at": "now()"
"inserted_at": "2020-11-11"
} |]
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing,
matchStatus = 200,
matchStatus = 204,
matchHeaders = []
}
describe "Patching record" $ do
context "to unkonwn uri" $
it "gives a 404" $
request methodPatch "/fake" []
[json| { "real": false } |]
`shouldRespondWith` 404
context "on an empty table" $
it "indicates no records found to update" $
request methodPatch "/simple_pk" []
[json| { "extra":20 } |]
`shouldRespondWith` 404
context "in a nonempty table" . before_ (clearTable "items" >> createItems 15) .
after_ (clearTable "items") $ do
it "can update a single item" $ do
g <- get "/items?id=eq.42"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "\\*/0"
request methodPatch "/items?id=eq.1" []
[json| { "id":42 } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing,
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "0-0/1"]
}
g' <- get "/items?id=eq.42"
liftIO $ simpleHeaders g'
`shouldSatisfy` matchHeader "Content-Range" "0-0/1"
it "can update multiple items" $ do
replicateM_ 10 $ post "/auto_incrementing_pk"
[json| { non_nullable_string: "a" } |]
replicateM_ 10 $ post "/auto_incrementing_pk"
[json| { non_nullable_string: "b" } |]
_ <- request methodPatch
"/auto_incrementing_pk?non_nullable_string=eq.a" []
[json| { non_nullable_string: "c" } |]
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
it "can update based on a computed column" $
request methodPatch
"/items?always_true=eq.false"
[("Prefer", "return=representation")]
[json| { id: 100 } |]
`shouldRespondWith` 404
it "can provide a representation" $ do
_ <- post "/items"
[json| { id: 1 } |]
request methodPatch
"/items?id=eq.1"
[("Prefer", "return=representation")]
[json| { id: 99 } |]
`shouldRespondWith` [json| [{id:99}] |]
describe "Row level permission" $
it "set user_id when inserting rows" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
p1 <- request methodPost "/authors_only"
[ authHeaderBasic "jdoe" "1234", ("Prefer", "return=representation") ]
[json| { "secret": "nyancat" } |]
liftIO $ do
simpleBody p1 `shouldBe` [json| { "owner":"jdoe", "secret":"nyancat" } |]
simpleStatus p1 `shouldBe` created201
p2 <- request methodPost "/authors_only"
-- jwt token for jroe
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.YuF_VfmyIxWyuceT7crnNKEprIYXsJAyXid3rjPjIow", ("Prefer", "return=representation") ]
[json| { "secret": "lolcat", "owner": "hacker" } |]
liftIO $ do
simpleBody p2 `shouldBe` [json| { "owner":"jroe", "secret":"lolcat" } |]
simpleStatus p2 `shouldBe` created201
+227 -15
View File
@@ -1,30 +1,242 @@
{-# LANGUAGE OverloadedStrings #-}
module Feature.QuerySpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders))
import SpecHelper
spec :: Spec
spec = around appWithFixture $ do
describe "Filtering response" $
context "column equality" $
spec =
beforeAll (clearTable "items" >> createItems 15)
. beforeAll (clearTable "nullable_integer" >> createNullInteger)
. beforeAll (
clearTable "no_pk" >>
createNulls 2 >>
createLikableStrings >>
createJsonData)
. afterAll_ (clearTable "items" >> clearTable "no_pk" >> clearTable "simple_pk")
. around withApp $ do
it "matches the predicate" $
get "/items?id=eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":5}]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
describe "Querying a table with a column called count" $
it "should not confuse count column with pg_catalog.count aggregate" $
get "/has_count_column" `shouldRespondWith` 200
describe "Canonical location" $
describe "Querying a nonexistent table" $
it "causes a 404" $
get "/faketable" `shouldRespondWith` 404
describe "Filtering response" $ do
it "matches with equality" $
get "/items?id=eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
it "matches with equality using not operator" $
get "/items?id=not.eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-13/14"]
}
it "matches with more than one condition using not operator" $
get "/simple_pk?k=like.*yx&extra=not.eq.u" `shouldRespondWith` "[]"
it "matches with inequality using not operator" $ do
get "/items?id=not.lt.14&order=id.asc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":14},{"id":15}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
get "/items?id=not.gt.2&order=id.asc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
it "matches items IN" $
get "/items?id=in.1,3,5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches items NOT IN" $
get "/items?id=notin.2,4,6,7,8,9,10,11,12,13,14,15"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches items NOT IN using not operator" $
get "/items?id=not.in.2,4,6,7,8,9,10,11,12,13,14,15"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches nulls using not operator" $
get "/no_pk?a=not.is.null" `shouldRespondWith`
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
it "matches nulls in varchar and numeric fields alike" $ do
get "/no_pk?a=is.null" `shouldRespondWith`
[json| [{"a": null, "b": null}] |]
get "/nullable_integer?a=is.null" `shouldRespondWith` "[{\"a\":null}]"
it "matches with like" $ do
get "/simple_pk?k=like.*yx" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]"
get "/simple_pk?k=like.xy*" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]"
get "/simple_pk?k=like.*YY*" `shouldRespondWith`
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]"
it "matches with like using not operator" $
get "/simple_pk?k=not.like.*yx" `shouldRespondWith`
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]"
it "matches with ilike" $ do
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
it "matches with ilike using not operator" $
get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]"
it "matches with tsearch @@" $
get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith`
[json| [{"text_search_vector":"'bar':2 'foo':1"}] |]
it "matches with tsearch @@ using not operator" $
get "/tsearch?text_search_vector=not.@@.foo" `shouldRespondWith`
[json| [{"text_search_vector":"'baz':1 'qux':2"}] |]
it "matches with computed column" $
get "/items?always_true=eq.true" `shouldRespondWith`
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
describe "ordering response" $ do
it "by a column asc" $
get "/items?id=lte.2&order=id.asc"
`shouldRespondWith` ResponseMatcher {
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=id.desc"
`shouldRespondWith` ResponseMatcher {
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
describe "Accept headers" $ do
it "should respond an unknown accept type with 415" $
request methodGet "/simple_pk"
(acceptHdrs "text/unknowntype") ""
`shouldRespondWith` 415
it "should respond correctly to */* in accept header" $
request methodGet "/simple_pk"
(acceptHdrs "*/*") ""
`shouldRespondWith` 200
it "should respond correctly to multiple types in accept header" $
request methodGet "/simple_pk"
(acceptHdrs "text/unknowntype, text/csv") ""
`shouldRespondWith` 200
it "should respond with CSV to 'text/csv' request" $
request methodGet "/simple_pk"
(acceptHdrs "text/csv; version=1") ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just "k,extra\rxyyx,u\rxYYx,v"
, matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv"]
}
describe "Canonical location" $ do
it "Sets Content-Location with alphabetized params" $
get "/no_pk?b=eq.1&a=eq.1"
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 204
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"]
}
it "Omits question mark when there are no params" $ do
r <- get "/simple_pk"
liftIO $ do
let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` matchHeader
"Content-Location" "/simple_pk"
describe "jsonb" $ do
it "can filter by properties inside json column" $ do
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
[json| [{"data": {"foo": {"bar": "baz"}}}] |]
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
[json| [] |]
it "can filter by properties inside json column using not" $
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
[json| [] |]
describe "remote procedure call" $ do
context "a proc that returns a set" . before_ (clearTable "items" >> createItems 10) .
after_ (clearTable "items") $
it "returns proper json" $
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
[json| [ {"id": 3}, {"id":4} ] |]
context "a proc that returns plain text" $
it "returns proper json" $
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
[json| [{"sayhello":"Hello, world"}] |]
+4 -4
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE OverloadedStrings #-}
module Feature.RangeSpec where
import Test.Hspec
@@ -9,7 +8,8 @@ import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
import SpecHelper
spec :: Spec
spec = around appWithFixture $
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
describe "GET /items" $ do
context "without range headers" $
@@ -37,8 +37,8 @@ spec = around appWithFixture $
request methodGet "/menagerie"
(rangeHdrs $ ByteRangeFromTo 0 1) ""
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 204
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/0"]
}
+35 -26
View File
@@ -1,38 +1,46 @@
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
module Feature.StructureSpec where
import Test.Hspec
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import SpecHelper
import Network.HTTP.Types
import Codec.Binary.Base64.String (encode)
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
spec :: Spec
spec = let {uName = "a user"; uPass = "nobody can ever know";
uRole = "dbapi_test"} in
around withDatabaseConnection $
aroundWith (withUser uName uPass uRole) $ aroundWith withApp $ do
describe "GET /" $
spec = around withApp $ do
describe "GET /" $ do
it "lists views in schema" $
request methodGet "/"
[("Authorization", "Basic "<>(cs.encode $ cs uName<>":"<>cs uPass))] ""
request methodGet "/" [] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"authors_only","insertable":true}
, {"schema":"1","name":"auto_incrementing_pk","insertable":true}
{"schema":"1","name":"auto_incrementing_pk","insertable":true}
, {"schema":"1","name":"compound_pk","insertable":true}
, {"schema":"1","name":"has_count_column","insertable":false}
, {"schema":"1","name":"has_fk","insertable":true}
, {"schema":"1","name":"insertable_view_with_join","insertable":true}
, {"schema":"1","name":"items","insertable":true}
, {"schema":"1","name":"json","insertable":true}
, {"schema":"1","name":"materialized_view","insertable":false}
, {"schema":"1","name":"menagerie","insertable":true}
, {"schema":"1","name":"no_pk","insertable":true}
, {"schema":"1","name":"nullable_integer","insertable":true}
, {"schema":"1","name":"simple_pk","insertable":true}
, {"schema":"1","name":"tsearch","insertable":true}
] |]
{matchStatus = 200}
it "lists only views user has permission to see" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
request methodGet "/" [auth] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"authors_only","insertable":true}
] |]
{matchStatus = 200}
describe "Table info" $ do
it "is available with OPTIONS verb" $
request methodOptions "/menagerie" [] "" `shouldRespondWith`
@@ -48,7 +56,7 @@ uRole = "dbapi_test"} in
"name": "integer",
"type": "integer",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"position": 1,
"references": null,
@@ -61,7 +69,7 @@ uRole = "dbapi_test"} in
"name": "double",
"type": "double precision",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"references": null,
"position": 2
@@ -73,7 +81,7 @@ uRole = "dbapi_test"} in
"name": "varchar",
"type": "character varying",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"position": 3,
"references": null,
@@ -86,7 +94,7 @@ uRole = "dbapi_test"} in
"name": "boolean",
"type": "boolean",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"references": null,
"position": 4
@@ -98,7 +106,7 @@ uRole = "dbapi_test"} in
"name": "date",
"type": "date",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"references": null,
"position": 5
@@ -110,7 +118,7 @@ uRole = "dbapi_test"} in
"name": "money",
"type": "money",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"position": 6,
"references": null,
@@ -136,9 +144,10 @@ uRole = "dbapi_test"} in
}
|]
it "includes foreign key data" $
request methodOptions "/has_fk"
[("Authorization", "Basic "<>(cs.encode $ cs uName<>":"<>cs uPass))] ""
it "includes foreign key data" $ do
pendingWith "have to resolve issue #107"
request methodOptions "/has_fk" [] ""
`shouldRespondWith` [json|
{
"pkey": ["id"],
@@ -153,7 +162,7 @@ uRole = "dbapi_test"} in
"maxLen": null,
"nullable": false,
"position": 1,
"enum": null,
"enum": [],
"references": null
}, {
"default": null,
@@ -165,7 +174,7 @@ uRole = "dbapi_test"} in
"maxLen": null,
"nullable": true,
"position": 2,
"enum": null,
"enum": [],
"references": {"table": "auto_incrementing_pk", "column": "id"}
}, {
"default": null,
@@ -177,7 +186,7 @@ uRole = "dbapi_test"} in
"maxLen": 255,
"nullable": true,
"position": 3,
"enum": null,
"enum": [],
"references": {"table": "simple_pk", "column": "k"}
}
]
+2 -11
View File
@@ -1,17 +1,8 @@
module Main where
import Database.HDBC (runRaw, disconnect)
import Test.Hspec
import SpecHelper
import Spec
import SpecHelper (openConnection, loadFixture)
main :: IO ()
main = do
c <-openConnection
runRaw c "drop schema if exists \"1\" cascade"
runRaw c "drop schema if exists private cascade"
runRaw c "drop schema if exists dbapi cascade"
loadFixture "roles" c
loadFixture "schema" c
disconnect c
hspec spec
main = resetDb >> hspec spec
+112 -62
View File
@@ -1,104 +1,154 @@
{-# LANGUAGE OverloadedStrings #-}
module SpecHelper where
import Network.Wai
import Test.Hspec
import Test.Hspec.Wai
import Database.HDBC
import Database.HDBC.PostgreSQL
import Hasql as H
import Hasql.Backend as B
import Hasql.Postgres as P
import Data.String.Conversions (cs)
import Control.Exception.Base (bracket, finally, tryJust)
import Control.Monad (when)
import Data.Monoid
import Data.Text hiding (map)
import qualified Data.Vector as V
import Control.Monad (void)
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
hRange, hAuthorization)
hRange, hAuthorization, hAccept)
import Codec.Binary.Base64.String (encode)
import Data.CaseInsensitive (CI(..))
import Data.Maybe (fromMaybe)
import Text.Regex.TDFA ((=~))
import qualified Data.HashMap.Strict as Hash
import qualified Data.ByteString.Char8 as BS
import Network.Wai.Middleware.Cors (cors)
import System.Process (readProcess)
import Dbapi (app, corsPolicy, AppConfig(..))
import PgQuery(addUser)
import qualified Data.Aeson.Types as J
import PostgREST.App (app)
import PostgREST.Config (AppConfig(..))
import PostgREST.Middleware
import PostgREST.Error(errResponse)
isLeft :: Either a b -> Bool
isLeft (Left _ ) = True
isLeft _ = False
cfg :: AppConfig
cfg = AppConfig "postgres://dbapi_test:@localhost:5432/dbapi_test" 9000 "test/test.crt" "test/test.key" "dbapi_anonymous"
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10 "1" "safe"
openConnection :: IO Connection
openConnection = connectPostgreSQL' $ configDbUri cfg
testPoolOpts :: PoolSettings
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
withDatabaseConnection :: (Connection -> IO ()) -> IO ()
withDatabaseConnection = bracket openConnection disconnect
pgSettings :: P.Settings
pgSettings = P.ParamSettings (cs $ configDbHost cfg)
(fromIntegral $ configDbPort cfg)
(cs $ configDbUser cfg)
(cs $ configDbPass cfg)
(cs $ configDbName cfg)
loadFixture :: String -> Connection -> IO ()
loadFixture name conn = do
sql <- readFile $ "test/fixtures/" ++ name ++ ".sql"
runRaw conn sql
withApp :: ActionWith Application -> IO ()
withApp perform = do
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings testPoolOpts
dbWithSchema :: ActionWith Connection -> IO ()
dbWithSchema action = withDatabaseConnection $ \c -> do
runRaw c "begin;"
action c
rollback c
perform $ middle $ \req resp -> do
body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx (Just (H.ReadCommitted, Just True))
$ authenticated cfg (app cfg body) req
either (resp . errResponse) resp result
withUser :: BS.ByteString -> BS.ByteString -> BS.ByteString ->
ActionWith Connection -> ActionWith Connection
withUser name pass role action conn = do
addUser name pass role conn
finally (action conn) $ do
_ <- run conn "delete from dbapi.auth where id=?" [toSql name]
runRaw conn "commit"
where middle = defaultMiddle False
withApp :: ActionWith Application -> ActionWith Connection
withApp action conn = do
runRaw conn "begin;"
action $ cors corsPolicy $ app conn "dbapi_anonymous"
rollback conn
appWithFixture :: ActionWith Application -> IO ()
appWithFixture action = withDatabaseConnection $ \c -> do
result <- tryJust transactionAborted $ do
runRaw c "begin;"
action $ cors corsPolicy $ app c "dbapi_anonymous"
rollback c
resetDb :: IO ()
resetDb = do
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $
H.tx Nothing $ do
H.unitEx [H.stmt| drop schema if exists "1" cascade |]
H.unitEx [H.stmt| drop schema if exists private cascade |]
H.unitEx [H.stmt| drop schema if exists postgrest cascade |]
when (isLeft result) $
putStrLn "note: commands ignored after aborted transaction"
loadFixture "roles"
loadFixture "schema"
loadFixture :: FilePath -> IO()
loadFixture name =
void $ readProcess "psql" ["-U", "postgrest_test", "-d", "postgrest_test", "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] []
where
transactionAborted :: SqlError -> Maybe ()
transactionAborted e =
if seState e == "25P02" then Just () else Nothing
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")
getHeader :: CI BS.ByteString -> [Header] -> Maybe BS.ByteString
getHeader name headers =
Hash.lookup name $ Hash.fromList headers
matchHeader :: CI BS.ByteString -> String -> [Header] -> Bool
matchHeader name valRegex headers =
maybe False (=~ valRegex) $ getHeader name headers
maybe False (=~ valRegex) $ lookup name headers
authHeader :: String -> String -> Header
authHeader user pass =
(hAuthorization, cs $ "Basic: " ++ encode (user ++ ":" ++ pass))
authHeaderBasic :: String -> String -> Header
authHeaderBasic u p =
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
-- for hspec-wai
pending_ :: WaiSession ()
pending_ = liftIO pending
authHeaderJWT :: String -> Header
authHeaderJWT token =
(hAuthorization, cs $ "Bearer " ++ token)
-- for hspec-wai
pendingWith_ :: String -> WaiSession ()
pendingWith_ = liftIO . pendingWith
testPool :: IO(H.Pool P.Postgres)
testPool = H.acquirePool pgSettings testPoolOpts
clearTable :: Text -> IO ()
clearTable table = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ B.Stmt ("delete from \"1\"."<>table) V.empty True
createItems :: Int -> IO ()
createItems n = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = mapM_ H.unitEx stmts
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
createNulls :: Int -> IO ()
createNulls n = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = mapM_ H.unitEx (stmt':stmts)
stmt' = [H.stmt|insert into "1".no_pk (a,b) values (null,null)|]
stmts = map [H.stmt|insert into "1".no_pk (a,b) values (?,0)|] [1..n]
createNullInteger :: IO ()
createNullInteger = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ [H.stmt| insert into "1".nullable_integer (a) values (null) |]
createLikableStrings :: IO ()
createLikableStrings = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $ do
H.unitEx $ insertSimplePk "xyyx" "u"
H.unitEx $ insertSimplePk "xYYx" "v"
where
insertSimplePk :: Text -> Text -> H.Stmt P.Postgres
insertSimplePk = [H.stmt|insert into "1".simple_pk (k, extra) values (?,?)|]
createJsonData :: IO ()
createJsonData = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $
[H.stmt|
insert into "1".json (data) values (?)
|]
(J.object [("foo", J.object [("bar", J.String "baz")])])
+32 -12
View File
@@ -1,16 +1,17 @@
{-# LANGUAGE OverloadedStrings #-}
module TestTypes (
IncPK(..),
fromList
IncPK(..)
, CompoundPK(..)
-- , incFromList
-- , compoundFromList
) where
import qualified Data.Aeson as JSON
import Data.Aeson ((.:))
import Data.Maybe (fromJust)
import Control.Applicative ((<$>), (<*>))
-- import Data.Maybe (fromJust)
import Control.Applicative
import Control.Monad (mzero)
import Database.HDBC (SqlValue, fromSql)
import Prelude
data IncPK = IncPK {
incId :: Int
@@ -27,9 +28,28 @@ instance JSON.FromJSON IncPK where
r .: "inserted_at"
parseJSON _ = mzero
fromList :: [(String, SqlValue)] -> IncPK
fromList row = IncPK
(fromSql . fromJust $ lookup "id" row)
(fromSql . fromJust $ lookup "nullable_string" row)
(fromSql . fromJust $ lookup "non_nullable_string" row)
(fromSql . fromJust $ lookup "inserted_at" row)
-- incFromList :: [(String, SqlValue)] -> IncPK
-- incFromList row = IncPK
-- (fromSql . fromJust $ lookup "id" row)
-- (fromSql . fromJust $ lookup "nullable_string" row)
-- (fromSql . fromJust $ lookup "non_nullable_string" row)
-- (fromSql . fromJust $ lookup "inserted_at" row)
data CompoundPK = CompoundPK {
compoundK1 :: Int
, compoundK2 :: Int
, compoundExtra :: Maybe Int
}
instance JSON.FromJSON CompoundPK where
parseJSON (JSON.Object r) = CompoundPK <$>
r .: "k1" <*>
r .: "k2" <*>
r .: "extra"
parseJSON _ = mzero
-- compoundFromList :: [(String, SqlValue)] -> CompoundPK
-- compoundFromList row = CompoundPK
-- (fromSql . fromJust $ lookup "k1" row)
-- (fromSql . fromJust $ lookup "k2" row)
-- (fromSql . fromJust $ lookup "extra" row)
@@ -1,15 +1,16 @@
{-# LANGUAGE OverloadedStrings #-}
module Unit.PgQuerySpec where
import Test.Hspec
import Test.QuickCheck
import Test.QuickCheck.Monadic
import Database.HDBC (IConnection, SqlValue, toSql, prepare,
quickQuery, fromSql, execute, seState, fetchAllRowsAL)
import PgQuery (LoginAttempt(..), insert, addUser, signInRole, checkPass)
import PgQuery (LoginAttempt(..), insert, addUser, signInRole, checkPass
, pgFmtIdent, pgFmtLit)
import Types (SqlRow(SqlRow))
import TestTypes (fromList, incStr, incNullableStr, incInsert, incId)
import TestTypes (incFromList, incStr, incNullableStr, incInsert, incId)
import Data.Map (toList)
import Data.String.Conversions (cs)
import Data.Monoid ((<>))
@@ -33,13 +34,13 @@ spec = around dbWithSchema $ do
it "inserts and responds with a full object description" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [
("non_nullable_string", toSql ("a string"::String))]) conn
let returnRow = fromList . toList $ r
let returnRow = incFromList . toList $ r
incStr returnRow `shouldBe` "a string"
incNullableStr returnRow `shouldBe` Nothing
incInsert returnRow `shouldSatisfy` not . null
incId returnRow `shouldSatisfy` (>= 0)
tRows <- quickALQuery conn "select * from \"1\".auto_incrementing_pk" []
[returnRow] `shouldBe` map fromList tRows
[returnRow] `shouldBe` map incFromList tRows
it "throws an exception if the PK is not unique" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [
@@ -53,11 +54,17 @@ spec = around dbWithSchema $ do
("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
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"}
describe "addUser" $ do
it "adds a correct user to the right table" $ \conn -> do
addUser user pass role conn
[r] <- quickQuery conn "select * from dbapi.auth" []
[r] <- quickQuery conn "select * from postgrest.auth" []
let [newUser, newRole, encryptedPass] = map fromSql r :: [String]
cs newUser `shouldBe` user
cs newRole `shouldBe` role
@@ -72,8 +79,21 @@ spec = around dbWithSchema $ do
addUser user pass role conn
return conn) $ do
it "accepts correct credentials and return the role" $ \conn ->
signInRole user pass conn `shouldReturn` LoginSuccess role
signInRole user pass conn `shouldReturn` LoginSuccess role user
it "returns nothing with bad creds" $ \conn -> do
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
signInRole user (pass <> "crap") conn `shouldReturn` LoginFailed
describe "pgFmtIdent" $
it "Does what format %I would do" $ \conn -> property $ \fuzz ->
monadicIO $ do
[[row]] <- run $ quickALQuery conn "select format('%I', ? :: varchar)" [toSql (fuzz :: String)]
assert $ fromSql (snd row) == pgFmtIdent (cs fuzz)
describe "pgFmtLit" $
it "Does what format %L would do" $ \conn ->
property $ monadicIO $ do
fuzz <- pick arbitrary
[[row]] <- run $ quickALQuery conn "select format('%L', ? :: varchar)" [toSql (fuzz :: String)]
assert $ fromSql (snd row) == pgFmtLit (cs fuzz)
@@ -14,7 +14,7 @@ spec = around dbWithSchema $ beforeWith setRole $ do
it "shows all the tables" $ \conn -> do
ts <- tables "1" conn
map tableName ts `shouldBe` ["authors_only","auto_incrementing_pk",
"compound_pk","has_fk","items","menagerie","no_pk", "simple_pk"]
"compound_pk","has_fk","insertable_view_with_join","items","menagerie","no_pk", "simple_pk"]
describe "columns" $ do
it "responds with each column for the table" $ \conn -> do
@@ -34,4 +34,4 @@ spec = around dbWithSchema $ beforeWith setRole $ do
("auto_inc_fk", ForeignKey {fkTable="auto_incrementing_pk", fkCol="id"}),
("simple_fk", ForeignKey { fkTable="simple_pk", fkCol="k"})]
where setRole conn = quickQuery conn "set role dbapi_test" [] >> return conn
where setRole conn = quickQuery conn "set role postgrest_test" [] >> return conn
+1 -1
View File
@@ -1 +1 @@
pg_dump --host localhost --port 5432 --username "postgres" --no-password --format plain --column-inserts --verbose --file "./test/fixtures/schema.sql" "dbapi_test"
pg_dump --host localhost --port 5432 --username "postgres" --no-password --format plain --column-inserts --verbose --file "./test/fixtures/schema.sql" "postgrest_test"
+3 -6
View File
@@ -11,9 +11,6 @@ BEGIN
END;
$$;
select pg_temp.create_role_if_not_exists('dbapi_anonymous', 'with nologin');
select pg_temp.create_role_if_not_exists('test_default_role', 'with nologin');
select pg_temp.create_role_if_not_exists('dbapi_test_author', 'with nologin');
select pg_temp.create_role_if_not_exists('dbapi_test_author_a', 'with nologin in role dbapi_test_author');
select pg_temp.create_role_if_not_exists('dbapi_test_author_b', 'with nologin in role dbapi_test_author');
select pg_temp.create_role_if_not_exists('postgrest_anonymous', 'with nologin') as a
, pg_temp.create_role_if_not_exists('test_default_role', 'with nologin') as b
, pg_temp.create_role_if_not_exists('postgrest_test_author', 'with nologin') into temp shh;
+278 -316
View File
@@ -1,81 +1,50 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 9.3.4
-- Dumped by pg_dump version 9.3.4
-- Started on 2014-10-01 13:41:39 PDT
-- Started on 2014-10-07 16:46:34 PDT
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;
--
-- TOC entry 11 (class 2615 OID 280932)
-- Name: 1; Type: SCHEMA; Schema: -; Owner: dbapi_test
--
CREATE SCHEMA "1";
ALTER SCHEMA "1" OWNER TO dbapi_test;
--
-- TOC entry 8 (class 2615 OID 50928)
-- Name: dbapi; Type: SCHEMA; Schema: -; Owner: dbapi_test
--
CREATE SCHEMA dbapi;
ALTER SCHEMA "1" OWNER TO postgrest_test;
ALTER SCHEMA dbapi OWNER TO dbapi_test;
CREATE SCHEMA postgrest;
ALTER SCHEMA postgrest OWNER TO postgrest_test;
--
-- TOC entry 9 (class 2615 OID 50929)
-- Name: private; Type: SCHEMA; Schema: -; Owner: dbapi_test
--
CREATE SCHEMA private;
ALTER SCHEMA private OWNER TO dbapi_test;
ALTER SCHEMA private OWNER TO postgrest_test;
--
-- TOC entry 187 (class 3079 OID 12018)
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
--
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
--
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
--
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
SET search_path = "1", pg_catalog;
--
-- TOC entry 571 (class 1247 OID 309112)
-- Name: enum_menagerie_type; Type: TYPE; Schema: 1; Owner: postgres
--
CREATE TYPE "1".enum_menagerie_type AS ENUM (
'foo',
'bar'
CREATE TYPE enum_menagerie_type AS ENUM (
'foo',
'bar'
);
--
-- Name: check_role_exists(); Type: FUNCTION; Schema: dbapi; Owner: dbapi_test
--
ALTER TYPE "1".enum_menagerie_type OWNER TO postgrest_test;
CREATE FUNCTION dbapi.check_role_exists() RETURNS trigger
SET search_path = postgrest, pg_catalog;
CREATE FUNCTION check_role_exists() RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
@@ -88,10 +57,10 @@ end
$$;
ALTER FUNCTION dbapi.check_role_exists() OWNER TO dbapi_test;
ALTER FUNCTION postgrest.check_role_exists() OWNER TO postgrest_test;
CREATE FUNCTION dbapi.update_owner() RETURNS trigger
CREATE FUNCTION update_owner() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
@@ -101,7 +70,29 @@ END;
$$;
ALTER FUNCTION dbapi.update_owner() OWNER TO dbapi_test;
ALTER FUNCTION postgrest.update_owner() OWNER TO postgrest_test;
CREATE FUNCTION set_authors_only_owner() RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
NEW.owner = current_setting('user_vars.user_id');
RETURN NEW;
end
$$;
ALTER FUNCTION postgrest.set_authors_only_owner() OWNER TO postgrest_test;
CREATE FUNCTION "1".insert_insertable_view_with_join() RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
INSERT INTO "1".auto_incrementing_pk (nullable_string, non_nullable_string) VALUES (NEW.nullable_string, NEW.non_nullable_string);
RETURN NEW;
end;
$$;
ALTER FUNCTION "1".insert_insertable_view_with_join() OWNER TO postgrest_test;
SET search_path = "1", pg_catalog;
@@ -109,18 +100,15 @@ SET default_tablespace = '';
SET default_with_oids = false;
--
CREATE TABLE authors_only (
owner character varying NOT NULL,
secret character varying NOT NULL
);
ALTER TABLE "1".authors_only OWNER TO dbapi_test_author;
ALTER TABLE "1".authors_only OWNER TO postgrest_test_author;
--
-- TOC entry 175 (class 1259 OID 280937)
--
CREATE TABLE auto_incrementing_pk (
id integer NOT NULL,
@@ -130,11 +118,8 @@ CREATE TABLE auto_incrementing_pk (
);
ALTER TABLE "1".auto_incrementing_pk OWNER TO dbapi_test;
ALTER TABLE "1".auto_incrementing_pk OWNER TO postgrest_test;
--
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE; Schema: 1; Owner: dbapi_test
--
CREATE SEQUENCE auto_incrementing_pk_id_seq
START WITH 1
@@ -144,17 +129,12 @@ CREATE SEQUENCE auto_incrementing_pk_id_seq
CACHE 1;
ALTER TABLE "1".auto_incrementing_pk_id_seq OWNER TO dbapi_test;
ALTER TABLE "1".auto_incrementing_pk_id_seq OWNER TO postgrest_test;
--
--
ALTER SEQUENCE auto_incrementing_pk_id_seq OWNED BY auto_incrementing_pk.id;
--
-- Name: compound_pk; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
--
CREATE TABLE compound_pk (
k1 integer NOT NULL,
@@ -163,11 +143,8 @@ CREATE TABLE compound_pk (
);
ALTER TABLE "1".compound_pk OWNER TO dbapi_test;
ALTER TABLE "1".compound_pk OWNER TO postgrest_test;
--
-- Name: has_fk; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
--
CREATE TABLE has_fk (
id bigint NOT NULL,
@@ -176,12 +153,8 @@ CREATE TABLE has_fk (
);
ALTER TABLE "1".has_fk OWNER TO dbapi_test;
ALTER TABLE "1".has_fk OWNER TO postgrest_test;
--
-- TOC entry 185 (class 1259 OID 50947)
-- Name: has_fk_id_seq; Type: SEQUENCE; Schema: 1; Owner: dbapi_test
--
CREATE SEQUENCE has_fk_id_seq
START WITH 1
@@ -191,32 +164,43 @@ CREATE SEQUENCE has_fk_id_seq
CACHE 1;
ALTER TABLE "1".has_fk_id_seq OWNER TO dbapi_test;
ALTER TABLE "1".has_fk_id_seq OWNER TO postgrest_test;
--
-- TOC entry 2042 (class 0 OID 0)
-- Dependencies: 185
-- Name: has_fk_id_seq; Type: SEQUENCE OWNED BY; Schema: 1; Owner: dbapi_test
--
ALTER SEQUENCE has_fk_id_seq OWNED BY has_fk.id;
CREATE MATERIALIZED VIEW "1".materialized_view AS
SELECT
version();
ALTER TABLE "1".materialized_view OWNER TO postgrest_test;
CREATE VIEW "1".insertable_view_with_join AS
SELECT has_fk.id,
has_fk.auto_inc_fk,
has_fk.simple_fk,
auto_incrementing_pk.nullable_string,
auto_incrementing_pk.non_nullable_string,
auto_incrementing_pk.inserted_at
FROM (has_fk
JOIN auto_incrementing_pk USING (id));
ALTER TABLE "1".insertable_view_with_join OWNER TO postgrest_test;
CREATE VIEW "1".has_count_column AS
SELECT 1 AS count;
ALTER TABLE "1".insertable_view_with_join OWNER TO postgrest_test;
--
-- TOC entry 186 (class 1259 OID 50949)
-- Name: items; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
--
CREATE TABLE items (
id bigint NOT NULL
);
ALTER TABLE "1".items OWNER TO dbapi_test;
ALTER TABLE "1".items OWNER TO postgrest_test;
--
-- Name: items_id_seq; Type: SEQUENCE; Schema: 1; Owner: dbapi_test
--
CREATE SEQUENCE items_id_seq
START WITH 1
@@ -226,18 +210,31 @@ CREATE SEQUENCE items_id_seq
CACHE 1;
ALTER TABLE "1".items_id_seq OWNER TO dbapi_test;
ALTER TABLE "1".items_id_seq OWNER TO postgrest_test;
--
-- Name: items_id_seq; Type: SEQUENCE OWNED BY; Schema: 1; Owner: dbapi_test
--
ALTER SEQUENCE items_id_seq OWNED BY items.id;
--
-- Name: menagerie; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
--
CREATE FUNCTION "1".getitemrange(min bigint, max bigint) RETURNS SETOF "1".items AS $$
SELECT * FROM "1".items WHERE id > $1 AND id <= $2;
$$ LANGUAGE SQL;
CREATE FUNCTION "1".sayhello(name text) RETURNS text AS $$
SELECT 'Hello, ' || $1;
$$ LANGUAGE SQL;
CREATE FUNCTION "1".problem() RETURNS void LANGUAGE plpgsql AS
$$
BEGIN
RAISE 'bad thing';
END;
$$;
CREATE TABLE menagerie (
"integer" integer NOT NULL,
@@ -246,15 +243,12 @@ CREATE TABLE menagerie (
"boolean" boolean NOT NULL,
date date NOT NULL,
money money NOT NULL,
enum "1".enum_menagerie_type not null
enum enum_menagerie_type NOT NULL
);
ALTER TABLE "1".menagerie OWNER TO dbapi_test;
ALTER TABLE "1".menagerie OWNER TO postgrest_test;
--
-- Name: no_pk; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
--
CREATE TABLE no_pk (
a character varying,
@@ -262,11 +256,16 @@ CREATE TABLE no_pk (
);
ALTER TABLE "1".no_pk OWNER TO dbapi_test;
ALTER TABLE "1".no_pk OWNER TO postgrest_test;
CREATE TABLE nullable_integer (
a integer
);
ALTER TABLE "1".nullable_integer OWNER TO postgrest_test;
--
-- Name: simple_pk; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
--
CREATE TABLE simple_pk (
k character varying NOT NULL,
@@ -274,13 +273,26 @@ CREATE TABLE simple_pk (
);
ALTER TABLE "1".simple_pk OWNER TO dbapi_test;
ALTER TABLE "1".simple_pk OWNER TO postgrest_test;
SET search_path = dbapi, pg_catalog;
--
-- Name: auth; Type: TABLE; Schema: dbapi; Owner: dbapi_test; Tablespace:
--
CREATE TABLE json
(
data json
);
ALTER TABLE "1".json OWNER TO postgrest_test;
CREATE TABLE tsearch (
text_search_vector tsvector
);
ALTER TABLE "1".tsearch OWNER TO postgrest_test;
SET search_path = postgrest, pg_catalog;
CREATE TABLE auth (
id character varying NOT NULL,
@@ -289,13 +301,10 @@ CREATE TABLE auth (
);
ALTER TABLE dbapi.auth OWNER TO dbapi_test;
ALTER TABLE postgrest.auth OWNER TO postgrest_test;
SET search_path = private, pg_catalog;
--
-- Name: articles; Type: TABLE; Schema: private; Owner: dbapi_test; Tablespace:
--
CREATE TABLE articles (
body text,
@@ -304,11 +313,8 @@ CREATE TABLE articles (
);
ALTER TABLE private.articles OWNER TO dbapi_test;
ALTER TABLE private.articles OWNER TO postgrest_test;
--
-- Name: articles_id_seq; Type: SEQUENCE; Schema: private; Owner: dbapi_test
--
CREATE SEQUENCE articles_id_seq
START WITH 1
@@ -318,230 +324,127 @@ CREATE SEQUENCE articles_id_seq
CACHE 1;
ALTER TABLE private.articles_id_seq OWNER TO dbapi_test;
ALTER TABLE private.articles_id_seq OWNER TO postgrest_test;
--
-- Name: articles_id_seq; Type: SEQUENCE OWNED BY; Schema: private; Owner: dbapi_test
--
ALTER SEQUENCE articles_id_seq OWNED BY articles.id;
SET search_path = "1", pg_catalog;
--
-- Name: id; Type: DEFAULT; Schema: 1; Owner: dbapi_test
--
ALTER TABLE ONLY auto_incrementing_pk ALTER COLUMN id SET DEFAULT nextval('auto_incrementing_pk_id_seq'::regclass);
--
-- TOC entry 1886 (class 2604 OID 50987)
-- Name: id; Type: DEFAULT; Schema: 1; Owner: dbapi_test
--
ALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass);
--
-- TOC entry 1887 (class 2604 OID 50988)
-- Name: id; Type: DEFAULT; Schema: 1; Owner: dbapi_test
--
ALTER TABLE ONLY items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass);
SET search_path = private, pg_catalog;
--
-- Name: id; Type: DEFAULT; Schema: private; Owner: dbapi_test
--
ALTER TABLE ONLY articles ALTER COLUMN id SET DEFAULT nextval('articles_id_seq'::regclass);
SET search_path = "1", pg_catalog;
--
-- TOC entry 2279 (class 0 OID 281006)
-- Dependencies: 186
-- Data for Name: authors_only; Type: TABLE DATA; Schema: 1; Owner: dbapi_test_author
--
--
-- TOC entry 2268 (class 0 OID 280937)
-- Dependencies: 175
-- TOC entry 2016 (class 0 OID 50932)
-- Dependencies: 181
-- Data for Name: auto_incrementing_pk; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
--
-- TOC entry 2270 (class 0 OID 280946)
-- Dependencies: 177
-- TOC entry 2051 (class 0 OID 0)
-- Dependencies: 182
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: dbapi_test
--
SELECT pg_catalog.setval('auto_incrementing_pk_id_seq', 50, true);
--
-- TOC entry 2018 (class 0 OID 50941)
-- Dependencies: 183
-- Data for Name: compound_pk; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
--
--
-- TOC entry 2271 (class 0 OID 280949)
-- Dependencies: 178
SELECT pg_catalog.setval('auto_incrementing_pk_id_seq', 1, true);
--
-- TOC entry 2052 (class 0 OID 0)
-- Dependencies: 185
-- Name: has_fk_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: dbapi_test
--
SELECT pg_catalog.setval('has_fk_id_seq', 1, false);
--
-- TOC entry 2021 (class 0 OID 50949)
-- Dependencies: 186
-- Data for Name: items; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
--
INSERT INTO items (id) VALUES (1);
INSERT INTO items (id) VALUES (2);
INSERT INTO items (id) VALUES (3);
INSERT INTO items (id) VALUES (4);
INSERT INTO items (id) VALUES (5);
INSERT INTO items (id) VALUES (6);
INSERT INTO items (id) VALUES (7);
INSERT INTO items (id) VALUES (8);
INSERT INTO items (id) VALUES (9);
INSERT INTO items (id) VALUES (10);
INSERT INTO items (id) VALUES (11);
INSERT INTO items (id) VALUES (12);
INSERT INTO items (id) VALUES (13);
INSERT INTO items (id) VALUES (14);
INSERT INTO items (id) VALUES (15);
SELECT pg_catalog.setval('items_id_seq', 1, true);
--
-- Name: items_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: dbapi_test
--
INSERT INTO tsearch (text_search_vector) VALUES ('''bar'':2 ''foo'':1');
INSERT INTO tsearch (text_search_vector) VALUES ('''baz'':1 ''qux'':2');
SELECT pg_catalog.setval('items_id_seq', 15, true);
SET search_path = postgrest, pg_catalog;
--
-- Data for Name: menagerie; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
--
--
-- Data for Name: no_pk; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
--
--
SET search_path = dbapi, pg_catalog;
--
-- Data for Name: auth; Type: TABLE DATA; Schema: dbapi; Owner: dbapi_test
--
SET search_path = private, pg_catalog;
--
--
--
-- Name: articles_id_seq; Type: SEQUENCE SET; Schema: private; Owner: dbapi_test
--
SELECT pg_catalog.setval('articles_id_seq', 1, false);
SET search_path = "1", pg_catalog;
--
-- Name: authors_only_pkey; Type: CONSTRAINT; Schema: 1; Owner: dbapi_test_author; Tablespace:
--
CREATE FUNCTION public.always_true("1".items) RETURNS boolean
LANGUAGE sql STABLE
AS $$ SELECT true $$;
ALTER FUNCTION public.always_true("1".items) OWNER TO postgrest_test;
ALTER TABLE ONLY authors_only
ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret);
CREATE TRIGGER insert_insertable_view_with_join INSTEAD OF INSERT ON "1".insertable_view_with_join FOR EACH ROW EXECUTE PROCEDURE "1".insert_insertable_view_with_join();
--
-- TOC entry 2144 (class 2606 OID 280990)
--
CREATE TRIGGER secrets_owner_track BEFORE INSERT OR UPDATE ON authors_only FOR EACH ROW EXECUTE PROCEDURE postgrest.set_authors_only_owner();
ALTER TABLE ONLY auto_incrementing_pk
ADD CONSTRAINT auto_incrementing_pk_pkey PRIMARY KEY (id);
--
-- Name: compound_pk_pkey; Type: CONSTRAINT; Schema: 1; Owner: dbapi_test; Tablespace:
--
ALTER TABLE ONLY compound_pk
ADD CONSTRAINT compound_pk_pkey PRIMARY KEY (k1, k2);
--
-- TOC entry 1900 (class 2606 OID 50995)
--
ALTER TABLE ONLY simple_pk
ADD CONSTRAINT contacts_pkey PRIMARY KEY (k);
--
--
ALTER TABLE ONLY has_fk
ADD CONSTRAINT has_fk_pkey PRIMARY KEY (id);
--
-- TOC entry 1896 (class 2606 OID 50999)
--
ALTER TABLE ONLY items
ADD CONSTRAINT items_pkey PRIMARY KEY (id);
--
--
ALTER TABLE ONLY menagerie
ADD CONSTRAINT menagerie_pkey PRIMARY KEY ("integer");
SET search_path = dbapi, pg_catalog;
SET search_path = postgrest, pg_catalog;
--
-- Name: auth_pkey; Type: CONSTRAINT; Schema: dbapi; Owner: dbapi_test; Tablespace:
--
ALTER TABLE ONLY auth
ADD CONSTRAINT auth_pkey PRIMARY KEY (id);
@@ -549,132 +452,191 @@ ALTER TABLE ONLY auth
SET search_path = private, pg_catalog;
--
--
ALTER TABLE ONLY articles
ADD CONSTRAINT articles_pkey PRIMARY KEY (id);
SET search_path = dbapi, pg_catalog;
SET search_path = postgrest, pg_catalog;
--
-- Name: ensure_auth_role_exists; Type: TRIGGER; Schema: dbapi; Owner: dbapi_test
--
CREATE CONSTRAINT TRIGGER ensure_auth_role_exists AFTER INSERT OR UPDATE ON auth NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE PROCEDURE check_role_exists();
SET search_path = private, pg_catalog;
--
-- Name: articles_owner_track; Type: TRIGGER; Schema: private; Owner: dbapi_test
--
CREATE TRIGGER articles_owner_track BEFORE INSERT OR UPDATE ON articles FOR EACH ROW EXECUTE PROCEDURE dbapi.update_owner();
CREATE TRIGGER articles_owner_track BEFORE INSERT OR UPDATE ON articles FOR EACH ROW EXECUTE PROCEDURE postgrest.update_owner();
SET search_path = "1", pg_catalog;
--
-- TOC entry 1905 (class 2606 OID 51009)
-- Name: has_fk_fk_fkey; Type: FK CONSTRAINT; Schema: 1; Owner: dbapi_test
--
ALTER TABLE ONLY has_fk
ADD CONSTRAINT has_fk_fk_fkey FOREIGN KEY (auto_inc_fk) REFERENCES auto_incrementing_pk(id);
--
-- TOC entry 1906 (class 2606 OID 51015)
-- Name: has_fk_simple_fk_fkey; Type: FK CONSTRAINT; Schema: 1; Owner: dbapi_test
--
ALTER TABLE ONLY has_fk
ADD CONSTRAINT has_fk_simple_fk_fkey FOREIGN KEY (simple_fk) REFERENCES simple_pk(k);
REVOKE ALL ON SCHEMA "1" FROM dbapi_test;
GRANT ALL ON SCHEMA "1" TO dbapi_test;
GRANT USAGE ON SCHEMA "1" TO dbapi_anonymous;
REVOKE ALL ON SCHEMA "1" FROM PUBLIC;
REVOKE ALL ON SCHEMA "1" FROM postgrest_test;
GRANT ALL ON SCHEMA "1" TO postgrest_test;
GRANT USAGE ON SCHEMA "1" TO postgrest_anonymous;
GRANT USAGE ON SCHEMA "1" TO postgrest_test_author;
REVOKE ALL ON SCHEMA postgrest FROM PUBLIC;
REVOKE ALL ON SCHEMA postgrest FROM postgrest_test;
GRANT ALL ON SCHEMA postgrest TO postgrest_test;
GRANT USAGE ON SCHEMA postgrest TO postgrest_anonymous;
--
-- TOC entry 2036 (class 0 OID 0)
-- Dependencies: 15
-- Name: public; Type: ACL; Schema: -; Owner: postgres
--
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM postgres;
GRANT ALL ON SCHEMA public TO postgres;
GRANT ALL ON SCHEMA public TO PUBLIC;
--
REVOKE ALL ON TABLE authors_only FROM dbapi_test_author;
GRANT ALL ON TABLE authors_only TO dbapi_test_author;
--
-- TOC entry 2290 (class 0 OID 0)
-- Dependencies: 175
-- Name: auto_incrementing_pk; Type: ACL; Schema: 1; Owner: dbapi_test
--
REVOKE ALL ON TABLE auto_incrementing_pk FROM dbapi_test;
GRANT ALL ON TABLE auto_incrementing_pk TO dbapi_test;
GRANT ALL ON TABLE auto_incrementing_pk TO dbapi_anonymous;
REVOKE ALL ON TABLE authors_only FROM PUBLIC;
REVOKE ALL ON TABLE authors_only FROM postgrest_test_author;
GRANT ALL ON TABLE authors_only TO postgrest_test_author;
REVOKE ALL ON SEQUENCE auto_incrementing_pk_id_seq FROM dbapi_test;
GRANT ALL ON SEQUENCE auto_incrementing_pk_id_seq TO dbapi_test;
GRANT USAGE ON SEQUENCE auto_incrementing_pk_id_seq TO dbapi_anonymous;
REVOKE ALL ON TABLE auto_incrementing_pk FROM PUBLIC;
REVOKE ALL ON TABLE auto_incrementing_pk FROM postgrest_test;
GRANT ALL ON TABLE auto_incrementing_pk TO postgrest_test;
GRANT ALL ON TABLE auto_incrementing_pk TO postgrest_anonymous;
REVOKE ALL ON TABLE compound_pk FROM dbapi_test;
GRANT ALL ON TABLE compound_pk TO dbapi_test;
GRANT ALL ON TABLE compound_pk TO dbapi_anonymous;
--
REVOKE ALL ON TABLE items FROM dbapi_test;
GRANT ALL ON TABLE items TO dbapi_test;
GRANT ALL ON TABLE items TO dbapi_anonymous;
REVOKE ALL ON SEQUENCE auto_incrementing_pk_id_seq FROM PUBLIC;
REVOKE ALL ON SEQUENCE auto_incrementing_pk_id_seq FROM postgrest_test;
GRANT ALL ON SEQUENCE auto_incrementing_pk_id_seq TO postgrest_test;
GRANT USAGE ON SEQUENCE auto_incrementing_pk_id_seq TO postgrest_anonymous;
REVOKE ALL ON SEQUENCE items_id_seq FROM dbapi_test;
GRANT ALL ON SEQUENCE items_id_seq TO dbapi_test;
GRANT USAGE ON SEQUENCE items_id_seq TO dbapi_anonymous;
REVOKE ALL ON TABLE compound_pk FROM PUBLIC;
REVOKE ALL ON TABLE compound_pk FROM postgrest_test;
GRANT ALL ON TABLE compound_pk TO postgrest_test;
GRANT ALL ON TABLE compound_pk TO postgrest_anonymous;
--
REVOKE ALL ON TABLE menagerie FROM dbapi_test;
GRANT ALL ON TABLE menagerie TO dbapi_test;
GRANT ALL ON TABLE menagerie TO dbapi_anonymous;
REVOKE ALL ON TABLE has_fk FROM PUBLIC;
REVOKE ALL ON TABLE has_fk FROM postgrest_test;
GRANT ALL ON TABLE has_fk TO postgrest_test;
GRANT ALL ON TABLE has_fk TO postgrest_anonymous;
--
REVOKE ALL ON TABLE no_pk FROM dbapi_test;
GRANT ALL ON TABLE no_pk TO dbapi_test;
GRANT ALL ON TABLE no_pk TO dbapi_anonymous;
REVOKE ALL ON TABLE items FROM PUBLIC;
REVOKE ALL ON TABLE items FROM postgrest_test;
GRANT ALL ON TABLE items TO postgrest_test;
GRANT ALL ON TABLE items TO postgrest_anonymous;
REVOKE ALL ON FUNCTION getitemrange(bigint, bigint) FROM PUBLIC;
REVOKE ALL ON FUNCTION getitemrange(bigint, bigint) FROM postgrest_test;
GRANT EXECUTE ON FUNCTION getitemrange(bigint, bigint) TO postgrest_test;
GRANT EXECUTE ON FUNCTION getitemrange(bigint, bigint) TO postgrest_anonymous;
REVOKE ALL ON FUNCTION sayhello(text) FROM PUBLIC;
REVOKE ALL ON FUNCTION sayhello(text) FROM postgrest_test;
GRANT EXECUTE ON FUNCTION sayhello(text) TO postgrest_test;
GRANT EXECUTE ON FUNCTION sayhello(text) TO postgrest_anonymous;
REVOKE ALL ON FUNCTION problem() FROM PUBLIC;
REVOKE ALL ON FUNCTION problem() FROM postgrest_test_author;
GRANT EXECUTE ON FUNCTION problem() TO postgrest_test_author;
REVOKE ALL ON SEQUENCE items_id_seq FROM PUBLIC;
REVOKE ALL ON SEQUENCE items_id_seq FROM postgrest_test;
GRANT ALL ON SEQUENCE items_id_seq TO postgrest_test;
GRANT USAGE ON SEQUENCE items_id_seq TO postgrest_anonymous;
REVOKE ALL ON TABLE simple_pk FROM dbapi_test;
GRANT ALL ON TABLE simple_pk TO dbapi_test;
GRANT ALL ON TABLE simple_pk TO dbapi_anonymous;
REVOKE ALL ON TABLE menagerie FROM PUBLIC;
REVOKE ALL ON TABLE menagerie FROM postgrest_test;
GRANT ALL ON TABLE menagerie TO postgrest_test;
GRANT ALL ON TABLE menagerie TO postgrest_anonymous;
REVOKE ALL ON TABLE no_pk FROM PUBLIC;
REVOKE ALL ON TABLE no_pk FROM postgrest_test;
GRANT ALL ON TABLE no_pk TO postgrest_test;
GRANT ALL ON TABLE no_pk TO postgrest_anonymous;
REVOKE ALL ON TABLE nullable_integer FROM PUBLIC;
REVOKE ALL ON TABLE nullable_integer FROM postgrest_test;
GRANT ALL ON TABLE nullable_integer TO postgrest_test;
GRANT ALL ON TABLE nullable_integer TO postgrest_anonymous;
REVOKE ALL ON TABLE simple_pk FROM PUBLIC;
REVOKE ALL ON TABLE simple_pk FROM postgrest_test;
GRANT ALL ON TABLE simple_pk TO postgrest_test;
GRANT ALL ON TABLE simple_pk TO postgrest_anonymous;
REVOKE ALL ON TABLE json FROM PUBLIC;
REVOKE ALL ON TABLE json FROM postgrest_test;
GRANT ALL ON TABLE json TO postgrest_test;
GRANT ALL ON TABLE json TO postgrest_anonymous;
REVOKE ALL ON TABLE tsearch FROM PUBLIC;
REVOKE ALL ON TABLE tsearch FROM postgrest_test;
GRANT ALL ON TABLE tsearch TO postgrest_test;
GRANT ALL ON TABLE tsearch TO postgrest_anonymous;
REVOKE ALL ON TABLE materialized_view FROM PUBLIC;
REVOKE ALL ON TABLE materialized_view FROM postgrest_test;
GRANT ALL ON TABLE materialized_view TO postgrest_test;
GRANT ALL ON TABLE materialized_view TO postgrest_anonymous;
REVOKE ALL ON TABLE insertable_view_with_join FROM PUBLIC;
REVOKE ALL ON TABLE insertable_view_with_join FROM postgrest_test;
GRANT ALL ON TABLE insertable_view_with_join TO postgrest_test;
GRANT ALL ON TABLE insertable_view_with_join TO postgrest_anonymous;
REVOKE ALL ON TABLE has_count_column FROM PUBLIC;
REVOKE ALL ON TABLE has_count_column FROM postgrest_test;
GRANT ALL ON TABLE has_count_column TO postgrest_test;
GRANT ALL ON TABLE has_count_column TO postgrest_anonymous;
REVOKE ALL ON FUNCTION public.always_true("1".items) FROM PUBLIC;
REVOKE ALL ON FUNCTION public.always_true("1".items) FROM postgrest_test;
GRANT ALL ON FUNCTION public.always_true("1".items) TO postgrest_test;
GRANT ALL ON FUNCTION public.always_true("1".items) TO postgrest_anonymous;
SET search_path = postgrest, pg_catalog;
REVOKE ALL ON TABLE auth FROM PUBLIC;
REVOKE ALL ON TABLE auth FROM postgrest_test;
GRANT ALL ON TABLE auth TO postgrest_test;
GRANT INSERT ON TABLE auth TO postgrest_anonymous;
SET search_path = private, pg_catalog;
REVOKE ALL ON TABLE articles FROM dbapi_test;
GRANT ALL ON TABLE articles TO dbapi_test;
--
REVOKE ALL ON TABLE articles FROM PUBLIC;
REVOKE ALL ON TABLE articles FROM postgrest_test;
GRANT ALL ON TABLE articles TO postgrest_test;
-14
View File
@@ -1,14 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICLTCCAZYCCQCj6GtISfdwNjANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQGEwJV
UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEzARBgNVBAoT
Ckxvb3AgUmVjdXIxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNDEwMDMyMDQyNDNa
Fw0xNTEwMDMyMDQyNDNaMFsxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQG
A1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UEChMKTG9vcCBSZWN1cjESMBAGA1UE
AxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5gFnTRBge
mXdkCMD+OycujZrCWOOLIDBqRr7kDbxVXqz/TKHRVx6bz88g9egzvR2HLyA418kd
dAu+lMmjrRv/k0Lnk/UvC1aj0huoHpOVUgOwy3qS4cE663uU5qsrgf4RDP7bLDcQ
FDW02SQ2n5ryv8nB9TSYpvQvYPiTMMKdrwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
AAQwkC+GSaGArAKdMIqs8/55KAjyNd11MupiCWsu1cwBJ4QJc1PxrYOMLMYnU06J
I0v1bJ6mG06/Js0r2FHM0NXSQO+7DLPWu4LchoBgFt4ZRm2+GbLzFrfu41yn5mJN
VeUxCBQ9hOrE8Kwe+/9IUUVPxlISF+YHOyF3DxWUViak
-----END CERTIFICATE-----
-15
View File
@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQC5gFnTRBgemXdkCMD+OycujZrCWOOLIDBqRr7kDbxVXqz/TKHR
Vx6bz88g9egzvR2HLyA418kddAu+lMmjrRv/k0Lnk/UvC1aj0huoHpOVUgOwy3qS
4cE663uU5qsrgf4RDP7bLDcQFDW02SQ2n5ryv8nB9TSYpvQvYPiTMMKdrwIDAQAB
AoGAZZD0Haub9S0b5KayXMCwnFmmEaEvvR47xATGQgGPS8LRv9sKgp9LwA4RH7/k
imeSglD4OIdVs421XH0ExlxjBiV5EzTCgLUyKbfA//xUy9ggXD1Ks4vIHL0c1DM4
g6/zylN7CQtt4Bb6YdWDSAUXIl3U5Dj1kG7BWuXfJTxCx7ECQQDyLxoQRNkPwKP/
xeOqfCgwyD52rnUJ7g9UViFcCU2ZLLEYxE4b3FECcwhOfUlxYvKdOx03Qts0taow
dPU/BEXHAkEAxBVyKwTJ0T6rQmcQEx3WuaBhSjXgZigUN4OdXMg+LSO+1MON6Nhm
J9ociy+xvfd0Cf3yABaAEU/hZ+Pmk2DI2QJBAO/XU8F+3VQrXH7l9HSXJppBBRM1
7HScDRRhhAIIuI+UYgJ8DjqrMpLxZu2MSBqBenHZ5DIhBMOrkVMR0PrKeWsCQCHh
gsSkIysgpP7oSALFmSCh8a2c+ZUtP7EH8NzjTLsH/iVNVOvy2FPygBQcvZ2RcF95
naMeQCq9nrkQy/qTMqECQQDa1WjWS2ngwqGzHovqiKRg3lQ8MFNlvlStAQl8+9h0
KJj8Rl6h+gO8eretBIe+y5j/hCC90xlJotzwO3jJF6Hc
-----END RSA PRIVATE KEY-----