Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
200e5a26cc | ||
|
|
b8bbaa7764 | ||
|
|
1470091f1c | ||
|
|
31738d745f | ||
|
|
f19d4300bc | ||
|
|
cd81e9346f | ||
|
|
01355f39a1 | ||
|
|
87298f580a | ||
|
|
3bfe64dd06 | ||
|
|
b9d3eedb9d | ||
|
|
bb4126bf3a | ||
|
|
2e440822cb | ||
|
|
13eed84f57 | ||
|
|
e5fed86965 | ||
|
|
3c5fab009b | ||
|
|
b858626e17 | ||
|
|
330cc91645 | ||
|
|
1037824e11 | ||
|
|
4cc08a11e7 | ||
|
|
358254639a | ||
|
|
43bc9bfa83 | ||
|
|
a779e9eb8b | ||
|
|
f67e195f76 | ||
|
|
508d722fb2 | ||
|
|
14d7364f4b | ||
|
|
bfbce27a65 | ||
|
|
00a23058c8 | ||
|
|
82c74ed21f | ||
|
|
5f0b4977da | ||
|
|
82214856b6 | ||
|
|
c09adb967a | ||
|
|
e5d420b2db | ||
|
|
ef021056c9 | ||
|
|
b7b082cd8e | ||
|
|
a02632f18c | ||
|
|
e43ad54dbf | ||
|
|
8af91e262c | ||
|
|
7b94fb608d | ||
|
|
cf176c4100 | ||
|
|
c61418635e | ||
|
|
dba827d1fd | ||
|
|
e315ad99b4 | ||
|
|
088df7e6be | ||
|
|
40eec0b2ff | ||
|
|
77bec52be7 | ||
|
|
155d1dee6b | ||
|
|
0548d65911 | ||
|
|
40a30d7b02 | ||
|
|
62af792add | ||
|
|
4cd2475bf2 | ||
|
|
fc4c792f9e | ||
|
|
c094e5a0fc | ||
|
|
9d0f3573c6 | ||
|
|
4496a95014 | ||
|
|
893b7a7126 | ||
|
|
3b23c4aa5b | ||
|
|
d466ea45ff | ||
|
|
7ba5363d25 | ||
|
|
f28b03f419 | ||
|
|
de772b9246 | ||
|
|
c28b26d949 | ||
|
|
c02dd4aa98 | ||
|
|
b0974a4e36 | ||
|
|
17acd134c7 | ||
|
|
d4a4bbf966 | ||
|
|
7b7babd1d1 | ||
|
|
072a6ce4c7 | ||
|
|
d5c1438c6e | ||
|
|
30e5032ade | ||
|
|
d7fe59f0b0 | ||
|
|
8a006f07a7 | ||
|
|
01ab540ffe | ||
|
|
de848f64fa | ||
|
|
52e689b830 | ||
|
|
ef3e2511fe | ||
|
|
6b4b763bc4 | ||
|
|
6b1c8b3e39 | ||
|
|
f3293cfac1 |
@@ -7,6 +7,24 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
## [0.3.1.1] - 2016-03-28
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Preserve unicode values in insert,update,rpc (regression) - @begriffs
|
||||||
|
- Prevent duplicate call to stored procs (regression) - @begriffs
|
||||||
|
- Allow SQL functions to generate registered JWT claims - @begriffs
|
||||||
|
- Terminate gracefully on SIGTERM (for use in Docker) - @recmo
|
||||||
|
- Relation detection fix for views that depend on multiple tables - @ruslantalpa
|
||||||
|
- Avoid count on plurality=singular and allow multiple Prefer values - @ruslantalpa
|
||||||
|
|
||||||
|
## [0.3.1.0] - 2016-02-28
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Prevent query error from infecting later connection - @begriffs, @ruslantalpa, @nikita-volkov, @jwiegley
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Applies range headers to RPC calls - @diogob
|
||||||
|
|
||||||
## [0.3.0.4] - 2016-02-12
|
## [0.3.0.4] - 2016-02-12
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
FROM debian:jessie
|
||||||
|
|
||||||
|
ENV POSTGREST_VERSION 0.3.1.0
|
||||||
|
ENV POSTGREST_SCHEMA public
|
||||||
|
ENV POSTGREST_ANONYMOUS postgres
|
||||||
|
ENV POSTGREST_JWT_SECRET thisisnotarealsecret
|
||||||
|
ENV POSTGREST_MAX_ROWS 1000000
|
||||||
|
ENV POSTGREST_POOL 200
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y tar xz-utils wget libpq-dev && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN wget http://github.com/begriffs/postgrest/releases/download/v${POSTGREST_VERSION}/postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \
|
||||||
|
tar --xz -xvf postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \
|
||||||
|
mv postgrest /usr/local/bin/postgrest && \
|
||||||
|
rm postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz
|
||||||
|
|
||||||
|
CMD exec postgrest postgres://${PG_ENV_POSTGRES_USER}:${PG_ENV_POSTGRES_PASSWORD}@${PG_PORT_5432_TCP_ADDR}:${PG_PORT_5432_TCP_PORT}/${PG_ENV_POSTGRES_DB} \
|
||||||
|
--port 3000 \
|
||||||
|
--schema ${POSTGREST_SCHEMA} \
|
||||||
|
--anonymous ${POSTGREST_ANONYMOUS} \
|
||||||
|
--pool ${POSTGREST_POOL} \
|
||||||
|
--jwt-secret ${POSTGREST_JWT_SECRET} \
|
||||||
|
--max-rows ${POSTGREST_MAX_ROWS}
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"POSTGREST_VER": {
|
"POSTGREST_VER": {
|
||||||
"description": "Version of PostgREST to deploy",
|
"description": "Version of PostgREST to deploy",
|
||||||
"value": "0.3.0.4"
|
"value": "0.3.1.1"
|
||||||
},
|
},
|
||||||
"DB_NAME": {
|
"DB_NAME": {
|
||||||
"description": "Database name",
|
"description": "Database name",
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ dependencies:
|
|||||||
- createdb -O postgrest_test -U ubuntu postgrest_test
|
- createdb -O postgrest_test -U ubuntu postgrest_test
|
||||||
override:
|
override:
|
||||||
- stack setup
|
- stack setup
|
||||||
|
- rm -fr $(stack path --dist-dir) $(stack path --local-install-root)
|
||||||
- stack install hlint packdeps cabal-install
|
- stack install hlint packdeps cabal-install
|
||||||
|
- stack build
|
||||||
- stack build --test --no-run-tests
|
- stack build --test --no-run-tests
|
||||||
|
|
||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
- stack test
|
- stack test
|
||||||
- git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes "$@"
|
- git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes "$@"
|
||||||
|
- stack exec -- cabal update
|
||||||
|
- stack exec --no-ghc-package-path -- cabal install --only-d --dry-run
|
||||||
- stack exec -- packdeps *.cabal || true
|
- stack exec -- packdeps *.cabal || true
|
||||||
- stack exec -- cabal check
|
- stack exec -- cabal check
|
||||||
- stack haddock --no-haddock-deps
|
- stack haddock --no-haddock-deps
|
||||||
|
|||||||
+2
-2
@@ -153,7 +153,7 @@ similar way to our ```POST``` example.
|
|||||||
|
|
||||||
<p>It's advisable to create a separate trigger for <code>UPDATE</code> and <code>INSERT</code>
|
<p>It's advisable to create a separate trigger for <code>UPDATE</code> and <code>INSERT</code>
|
||||||
avoiding conditionals that decide which is the trigger current operation.
|
avoiding conditionals that decide which is the trigger current operation.
|
||||||
This makes it easier to change code for (or even disable) one operation without intefering with others while
|
This makes it easier to change code for (or even disable) one operation without interfering with others while
|
||||||
improving readability.
|
improving readability.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,7 +186,7 @@ basic field replacements, and not at all "incorrect."
|
|||||||
* ❌ Cannot be cached or prefetched
|
* ❌ Cannot be cached or prefetched
|
||||||
* ✅ Idempotent
|
* ✅ Idempotent
|
||||||
|
|
||||||
Simply use the `DELETE` verb. All recors that match your filter
|
Simply use the `DELETE` verb. All records that match your filter
|
||||||
will be removed. For instance deleting inactive users:
|
will be removed. For instance deleting inactive users:
|
||||||
|
|
||||||
```HTTP
|
```HTTP
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ ALTER TABLE users ADD role text NOT NULL DEFAULT 'customer';
|
|||||||
```
|
```
|
||||||
|
|
||||||
Besides the main user that PostgREST uses to connect to PostgreSQL
|
Besides the main user that PostgREST uses to connect to PostgreSQL
|
||||||
and the anonymous user, we will need two aditional roles for our example:
|
and the anonymous user, we will need two additional roles for our example:
|
||||||
|
|
||||||
* admin - to be used by users that access all the system rows.
|
* admin - to be used by users that access all the system rows.
|
||||||
* customer - to be used when user has restricted access to database rows.
|
* customer - to be used when user has restricted access to database rows.
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ a username and password system on top of JWT using only plpgsql.
|
|||||||
Future examples such as the multi-tenant blogging platform will use
|
Future examples such as the multi-tenant blogging platform will use
|
||||||
the results from this example for their auth. We will build a system
|
the results from this example for their auth. We will build a system
|
||||||
for users to sign up, log in, manage their accounts, and for admins
|
for users to sign up, log in, manage their accounts, and for admins
|
||||||
to manange other people's accounts. We will also see how to trigger
|
to manage other people's accounts. We will also see how to trigger
|
||||||
outside events like sending password reset emails.
|
outside events like sending password reset emails.
|
||||||
|
|
||||||
Before jumping into the code, a little more about how the tokens
|
Before jumping into the code, a little more about how the tokens
|
||||||
work. Every JWT contains cryptographically signed *claims*. PostgREST
|
work. Every JWT contains cryptographically signed *claims*. PostgREST
|
||||||
cares specificaly about a claim called `role`. When a client includes
|
cares specifically about a claim called `role`. When a client includes
|
||||||
a `role` claim PostgREST executes their request using that database
|
a `role` claim PostgREST executes their request using that database
|
||||||
role.
|
role.
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ begin
|
|||||||
where token_type = 'reset'
|
where token_type = 'reset'
|
||||||
and tokens.email = reset_password.email;
|
and tokens.email = reset_password.email;
|
||||||
|
|
||||||
select uuid_generate_v4() into tok;
|
select gen_random_uuid() into tok;
|
||||||
insert into basic_auth.tokens (token, token_type, email)
|
insert into basic_auth.tokens (token, token_type, email)
|
||||||
values (tok, 'reset', reset_password.email);
|
values (tok, 'reset', reset_password.email);
|
||||||
perform pg_notify('reset',
|
perform pg_notify('reset',
|
||||||
@@ -251,7 +251,7 @@ basic_auth.send_validation() returns trigger
|
|||||||
declare
|
declare
|
||||||
tok uuid;
|
tok uuid;
|
||||||
begin
|
begin
|
||||||
select uuid_generate_v4() into tok;
|
select gen_random_uuid() into tok;
|
||||||
insert into basic_auth.tokens (token, token_type, email)
|
insert into basic_auth.tokens (token, token_type, email)
|
||||||
values (tok, 'validation', new.email);
|
values (tok, 'validation', new.email);
|
||||||
perform pg_notify('validate',
|
perform pg_notify('validate',
|
||||||
@@ -294,7 +294,7 @@ where actual.role = member_of.rolname;
|
|||||||
-- is equal to email so that user can only see themselves
|
-- is equal to email so that user can only see themselves
|
||||||
```
|
```
|
||||||
|
|
||||||
Using this view clients can see themeslves and any other users with
|
Using this view clients can see themselves and any other users with
|
||||||
the right db roles. This view does not yet support inserts or updates
|
the right db roles. This view does not yet support inserts or updates
|
||||||
because not all the columns refer directly to underlying columns.
|
because not all the columns refer directly to underlying columns.
|
||||||
Nor do we want it to be auto-updatable because it would allow an escalation
|
Nor do we want it to be auto-updatable because it would allow an escalation
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ sudo apt-get install -y libpq-dev
|
|||||||
```bash
|
```bash
|
||||||
git clone https://github.com/begriffs/postgrest.git
|
git clone https://github.com/begriffs/postgrest.git
|
||||||
cd postgrest
|
cd postgrest
|
||||||
sudo stack install --install-ghc --local-bin-path /usr/local/bin
|
stack build --install-ghc
|
||||||
|
sudo stack install --allow-different-user --local-bin-path /usr/local/bin
|
||||||
```
|
```
|
||||||
|
|
||||||
* Run the server
|
* Run the server
|
||||||
@@ -94,7 +95,7 @@ The complete list of options:
|
|||||||
<code>secret</code> but do not use the default in production!
|
<code>secret</code> but do not use the default in production!
|
||||||
Load-balanced PostgREST servers should share the same secret.</dd>
|
Load-balanced PostgREST servers should share the same secret.</dd>
|
||||||
|
|
||||||
<dt>-p, --pool</dt>
|
<dt>-o, --pool</dt>
|
||||||
<dd>Max connections to use in db pool. Defaults to to 10, but you
|
<dd>Max connections to use in db pool. Defaults to to 10, but you
|
||||||
should find an optimal value for your db by running the SQL
|
should find an optimal value for your db by running the SQL
|
||||||
command <code>show max_connections;</code></dd>
|
command <code>show max_connections;</code></dd>
|
||||||
|
|||||||
+35
-10
@@ -2,7 +2,7 @@ name: postgrest
|
|||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for the tables and views, supporting all HTTP verbs that security
|
for the tables and views, supporting all HTTP verbs that security
|
||||||
permits.
|
permits.
|
||||||
version: 0.3.0.4
|
version: 0.3.1.1
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
@@ -23,10 +23,10 @@ Flag CI
|
|||||||
|
|
||||||
executable postgrest
|
executable postgrest
|
||||||
main-is: PostgREST/Main.hs
|
main-is: PostgREST/Main.hs
|
||||||
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes, LambdaCase
|
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
|
||||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
build-depends: aeson >= 0.8 && < 0.10
|
build-depends: aeson (>= 0.8 && < 0.10) || (>= 0.11 && < 0.12)
|
||||||
, base >= 4.8 && < 5
|
, base >= 4.8 && < 5
|
||||||
, bytestring
|
, bytestring
|
||||||
, case-insensitive
|
, case-insensitive
|
||||||
@@ -34,15 +34,19 @@ executable postgrest
|
|||||||
, containers
|
, containers
|
||||||
, contravariant
|
, contravariant
|
||||||
, errors
|
, errors
|
||||||
, hasql >= 0.19.3.3 && < 0.20
|
, hasql >= 0.19.9 && < 0.20
|
||||||
|
, hasql-pool >= 0.4 && < 0.5
|
||||||
|
, hasql-transaction >= 0.4.3 && < 0.5
|
||||||
, http-types
|
, http-types
|
||||||
, interpolatedstring-perl6
|
, interpolatedstring-perl6
|
||||||
, jwt
|
, jwt
|
||||||
|
, lens >=3.8 && < 5.0
|
||||||
|
, lens-aeson >= 1.0.0.0 && < 1.1.0.0
|
||||||
|
, mtl
|
||||||
, optparse-applicative >= 0.11 && < 0.13
|
, optparse-applicative >= 0.11 && < 0.13
|
||||||
, parsec
|
, parsec
|
||||||
, postgrest
|
, postgrest
|
||||||
, regex-tdfa
|
, regex-tdfa
|
||||||
, resource-pool
|
|
||||||
, safe >= 0.3 && < 0.4
|
, safe >= 0.3 && < 0.4
|
||||||
, scientific
|
, scientific
|
||||||
, string-conversions
|
, string-conversions
|
||||||
@@ -86,9 +90,14 @@ library
|
|||||||
, contravariant
|
, contravariant
|
||||||
, errors
|
, errors
|
||||||
, hasql
|
, hasql
|
||||||
|
, hasql-transaction
|
||||||
|
, hasql-pool
|
||||||
, http-types
|
, http-types
|
||||||
, interpolatedstring-perl6
|
, interpolatedstring-perl6
|
||||||
, jwt
|
, jwt
|
||||||
|
, lens
|
||||||
|
, lens-aeson
|
||||||
|
, mtl
|
||||||
, optparse-applicative
|
, optparse-applicative
|
||||||
, parsec
|
, parsec
|
||||||
, regex-tdfa
|
, regex-tdfa
|
||||||
@@ -99,12 +108,13 @@ library
|
|||||||
, time
|
, time
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
, vector
|
, vector
|
||||||
, wai
|
|
||||||
, wai-cors
|
|
||||||
, wai-extra
|
|
||||||
, wai-middleware-static
|
|
||||||
, HTTP
|
, HTTP
|
||||||
, Ranged-sets
|
, Ranged-sets
|
||||||
|
, wai >= 3.0.1
|
||||||
|
, wai-cors
|
||||||
|
, wai-extra
|
||||||
|
, wai-middleware-static >= 0.6.0
|
||||||
|
, warp >= 3.1.0
|
||||||
|
|
||||||
Other-Modules: Paths_postgrest
|
Other-Modules: Paths_postgrest
|
||||||
Exposed-Modules: PostgREST.App
|
Exposed-Modules: PostgREST.App
|
||||||
@@ -123,16 +133,20 @@ library
|
|||||||
Test-Suite spec
|
Test-Suite spec
|
||||||
Type: exitcode-stdio-1.0
|
Type: exitcode-stdio-1.0
|
||||||
Default-Language: Haskell2010
|
Default-Language: Haskell2010
|
||||||
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes, LambdaCase
|
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
|
||||||
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
Hs-Source-Dirs: test, src
|
Hs-Source-Dirs: test, src
|
||||||
Main-Is: Main.hs
|
Main-Is: Main.hs
|
||||||
Other-Modules: Feature.AuthSpec
|
Other-Modules: Feature.AuthSpec
|
||||||
|
, Feature.ConcurrentSpec
|
||||||
, Feature.CorsSpec
|
, Feature.CorsSpec
|
||||||
, Feature.DeleteSpec
|
, Feature.DeleteSpec
|
||||||
, Feature.InsertSpec
|
, Feature.InsertSpec
|
||||||
, Feature.QuerySpec
|
, Feature.QuerySpec
|
||||||
|
, Feature.QueryLimitedSpec
|
||||||
, Feature.RangeSpec
|
, Feature.RangeSpec
|
||||||
, Feature.StructureSpec
|
, Feature.StructureSpec
|
||||||
|
, Feature.UnicodeSpec
|
||||||
, Paths_postgrest
|
, Paths_postgrest
|
||||||
, PostgREST.App
|
, PostgREST.App
|
||||||
, PostgREST.Auth
|
, PostgREST.Auth
|
||||||
@@ -148,6 +162,7 @@ Test-Suite spec
|
|||||||
, SpecHelper
|
, SpecHelper
|
||||||
, TestTypes
|
, TestTypes
|
||||||
Build-Depends: aeson
|
Build-Depends: aeson
|
||||||
|
, async
|
||||||
, base
|
, base
|
||||||
, base64-string
|
, base64-string
|
||||||
, bytestring
|
, bytestring
|
||||||
@@ -157,6 +172,8 @@ Test-Suite spec
|
|||||||
, contravariant
|
, contravariant
|
||||||
, errors
|
, errors
|
||||||
, hasql
|
, hasql
|
||||||
|
, hasql-pool
|
||||||
|
, hasql-transaction
|
||||||
, heredoc
|
, heredoc
|
||||||
, hspec == 2.2.*
|
, hspec == 2.2.*
|
||||||
, hspec-wai
|
, hspec-wai
|
||||||
@@ -164,6 +181,10 @@ Test-Suite spec
|
|||||||
, http-types
|
, http-types
|
||||||
, interpolatedstring-perl6
|
, interpolatedstring-perl6
|
||||||
, jwt
|
, jwt
|
||||||
|
, lens
|
||||||
|
, lens-aeson
|
||||||
|
, monad-control
|
||||||
|
, mtl
|
||||||
, optparse-applicative
|
, optparse-applicative
|
||||||
, parsec
|
, parsec
|
||||||
, process
|
, process
|
||||||
@@ -173,11 +194,15 @@ Test-Suite spec
|
|||||||
, string-conversions
|
, string-conversions
|
||||||
, text
|
, text
|
||||||
, time
|
, time
|
||||||
|
, transformers
|
||||||
|
, transformers-base
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
|
, unix
|
||||||
, vector
|
, vector
|
||||||
, wai
|
, wai
|
||||||
, wai-cors
|
, wai-cors
|
||||||
, wai-extra
|
, wai-extra
|
||||||
, wai-middleware-static
|
, wai-middleware-static
|
||||||
|
, warp
|
||||||
, HTTP
|
, HTTP
|
||||||
, Ranged-sets
|
, Ranged-sets
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ create role authenticator noinherit;
|
|||||||
grant anon, author to authenticator;
|
grant anon, author to authenticator;
|
||||||
|
|
||||||
create extension if not exists pgcrypto;
|
create extension if not exists pgcrypto;
|
||||||
create extension if not exists "uuid-ossp";
|
|
||||||
|
|
||||||
-- We put things inside the basic_auth schema to hide
|
-- We put things inside the basic_auth schema to hide
|
||||||
-- them from public view. Certain public procs/views will
|
-- them from public view. Certain public procs/views will
|
||||||
@@ -97,7 +96,7 @@ basic_auth.send_validation() returns trigger
|
|||||||
declare
|
declare
|
||||||
tok uuid;
|
tok uuid;
|
||||||
begin
|
begin
|
||||||
select uuid_generate_v4() into tok;
|
select gen_random_uuid() into tok;
|
||||||
insert into basic_auth.tokens (token, token_type, email)
|
insert into basic_auth.tokens (token, token_type, email)
|
||||||
values (tok, 'validation', new.email);
|
values (tok, 'validation', new.email);
|
||||||
perform pg_notify('validate',
|
perform pg_notify('validate',
|
||||||
@@ -175,7 +174,7 @@ begin
|
|||||||
where token_type = 'reset'
|
where token_type = 'reset'
|
||||||
and tokens.email = request_password_reset.email;
|
and tokens.email = request_password_reset.email;
|
||||||
|
|
||||||
select uuid_generate_v4() into tok;
|
select gen_random_uuid() into tok;
|
||||||
insert into basic_auth.tokens (token, token_type, email)
|
insert into basic_auth.tokens (token, token_type, email)
|
||||||
values (tok, 'reset', request_password_reset.email);
|
values (tok, 'reset', request_password_reset.email);
|
||||||
perform pg_notify('reset',
|
perform pg_notify('reset',
|
||||||
@@ -215,7 +214,7 @@ begin
|
|||||||
where token_type = 'reset'
|
where token_type = 'reset'
|
||||||
and tokens.email = reset_password.email;
|
and tokens.email = reset_password.email;
|
||||||
|
|
||||||
select uuid_generate_v4() into tok;
|
select gen_random_uuid() into tok;
|
||||||
insert into basic_auth.tokens (token, token_type, email)
|
insert into basic_auth.tokens (token, token_type, email)
|
||||||
values (tok, 'reset', reset_password.email);
|
values (tok, 'reset', reset_password.email);
|
||||||
perform pg_notify('reset',
|
perform pg_notify('reset',
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ data PreferRepresentation = Full | HeadersOnly | None deriving Eq
|
|||||||
-- route responses and upload payloads
|
-- route responses and upload payloads
|
||||||
data ContentType = ApplicationJSON | TextCSV deriving Eq
|
data ContentType = ApplicationJSON | TextCSV deriving Eq
|
||||||
instance Show ContentType where
|
instance Show ContentType where
|
||||||
show ApplicationJSON = "application/json"
|
show ApplicationJSON = "application/json; charset=utf-8"
|
||||||
show TextCSV = "text/csv"
|
show TextCSV = "text/csv; charset=utf-8"
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Describes what the user wants to do. This data type is a
|
Describes what the user wants to do. This data type is a
|
||||||
@@ -130,7 +130,7 @@ userApiRequest schema req reqBody =
|
|||||||
, iPayload = relevantPayload
|
, iPayload = relevantPayload
|
||||||
, iPreferRepresentation = representation
|
, iPreferRepresentation = representation
|
||||||
, iPreferSingular = singular
|
, iPreferSingular = singular
|
||||||
, iPreferCount = not $ hasPrefer "count=none"
|
, iPreferCount = not $ singular || hasPrefer "count=none"
|
||||||
, iFilters = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ]
|
, iFilters = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ]
|
||||||
, iSelect = if method == "DELETE"
|
, iSelect = if method == "DELETE"
|
||||||
then "*"
|
then "*"
|
||||||
@@ -145,7 +145,11 @@ userApiRequest schema req reqBody =
|
|||||||
hdrs = requestHeaders req
|
hdrs = requestHeaders req
|
||||||
qParams = [(cs k, cs <$> v)|(k,v) <- queryString req]
|
qParams = [(cs k, cs <$> v)|(k,v) <- queryString req]
|
||||||
lookupHeader = flip lookup hdrs
|
lookupHeader = flip lookup hdrs
|
||||||
hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
|
hasPrefer :: T.Text -> Bool
|
||||||
|
hasPrefer val = any (\(h,v) -> h == "Prefer" && val `elem` split v) hdrs
|
||||||
|
where
|
||||||
|
split :: BS.ByteString -> [T.Text]
|
||||||
|
split = map T.strip . T.split (==';') . cs
|
||||||
singular = hasPrefer "plurality=singular"
|
singular = hasPrefer "plurality=singular"
|
||||||
representation
|
representation
|
||||||
| hasPrefer "return=representation" = Full
|
| hasPrefer "return=representation" = Full
|
||||||
|
|||||||
+54
-29
@@ -3,7 +3,7 @@
|
|||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
--module PostgREST.App where
|
--module PostgREST.App where
|
||||||
module PostgREST.App (
|
module PostgREST.App (
|
||||||
app
|
postgrest
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
@@ -18,6 +18,9 @@ import Data.String.Conversions (cs)
|
|||||||
import Data.Text (Text, replace, strip)
|
import Data.Text (Text, replace, strip)
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
|
|
||||||
|
import qualified Hasql.Pool as P
|
||||||
|
import qualified Hasql.Transaction as HT
|
||||||
|
|
||||||
import Text.Parsec.Error
|
import Text.Parsec.Error
|
||||||
import Text.ParserCombinators.Parsec (parse)
|
import Text.ParserCombinators.Parsec (parse)
|
||||||
|
|
||||||
@@ -26,25 +29,26 @@ import Network.HTTP.Types.Header
|
|||||||
import Network.HTTP.Types.Status
|
import Network.HTTP.Types.Status
|
||||||
import Network.HTTP.Types.URI (parseSimpleQuery)
|
import Network.HTTP.Types.URI (parseSimpleQuery)
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
|
import Network.Wai.Middleware.RequestLogger (logStdout)
|
||||||
|
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Aeson.Types (emptyArray)
|
import Data.Aeson.Types (emptyArray)
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Transaction as H
|
||||||
|
|
||||||
import PostgREST.Config (AppConfig (..))
|
|
||||||
import PostgREST.Parsers
|
|
||||||
import PostgREST.DbStructure
|
|
||||||
import PostgREST.RangeQuery
|
|
||||||
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..)
|
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..)
|
||||||
, Action(..), Target(..)
|
, Action(..), Target(..)
|
||||||
, PreferRepresentation (..)
|
, PreferRepresentation (..)
|
||||||
, userApiRequest)
|
, userApiRequest)
|
||||||
import PostgREST.Types
|
|
||||||
import PostgREST.Auth (tokenJWT)
|
import PostgREST.Auth (tokenJWT)
|
||||||
import PostgREST.Error (errResponse)
|
import PostgREST.Config (AppConfig (..))
|
||||||
|
import PostgREST.DbStructure
|
||||||
|
import PostgREST.Error (errResponse, pgErrResponse)
|
||||||
|
import PostgREST.Parsers
|
||||||
|
import PostgREST.RangeQuery
|
||||||
|
import PostgREST.Middleware
|
||||||
import PostgREST.QueryBuilder ( callProc
|
import PostgREST.QueryBuilder ( callProc
|
||||||
, addJoinConditions
|
, addJoinConditions
|
||||||
, sourceCTEName
|
, sourceCTEName
|
||||||
@@ -55,10 +59,25 @@ import PostgREST.QueryBuilder ( callProc
|
|||||||
, createWriteStatement
|
, createWriteStatement
|
||||||
, ResultsWithCount
|
, ResultsWithCount
|
||||||
)
|
)
|
||||||
|
import PostgREST.Types
|
||||||
|
|
||||||
import Prelude
|
import Prelude
|
||||||
|
|
||||||
app :: DbStructure -> AppConfig -> RequestBody -> Request -> H.Session Response
|
|
||||||
|
postgrest :: AppConfig -> DbStructure -> P.Pool -> Application
|
||||||
|
postgrest conf dbStructure pool =
|
||||||
|
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
|
||||||
|
|
||||||
|
middle $ \ req respond -> do
|
||||||
|
time <- getPOSIXTime
|
||||||
|
body <- strictRequestBody req
|
||||||
|
|
||||||
|
let handleReq = runWithClaims conf time (app dbStructure conf body) req
|
||||||
|
resp <- either pgErrResponse id <$> P.use pool
|
||||||
|
(HT.run handleReq HT.ReadCommitted HT.Write)
|
||||||
|
respond resp
|
||||||
|
|
||||||
|
app :: DbStructure -> AppConfig -> RequestBody -> Request -> H.Transaction Response
|
||||||
app dbStructure conf reqBody req =
|
app dbStructure conf reqBody req =
|
||||||
let
|
let
|
||||||
-- TODO: blow up for Left values (there is a middleware that checks the headers)
|
-- TODO: blow up for Left values (there is a middleware that checks the headers)
|
||||||
@@ -71,13 +90,10 @@ app dbStructure conf reqBody req =
|
|||||||
case readSqlParts of
|
case readSqlParts of
|
||||||
Left e -> return $ responseLBS status400 [jsonH] $ cs e
|
Left e -> return $ responseLBS status400 [jsonH] $ cs e
|
||||||
Right (q, cq) -> do
|
Right (q, cq) -> do
|
||||||
let range = restrictRange (configMaxRows conf) $ iRange apiRequest
|
let singular = iPreferSingular apiRequest
|
||||||
singular = iPreferSingular apiRequest
|
|
||||||
stm = createReadStatement q cq range singular
|
stm = createReadStatement q cq range singular
|
||||||
(iPreferCount apiRequest) (contentType == TextCSV)
|
shouldCount (contentType == TextCSV)
|
||||||
if range == emptyRange
|
respondToRange $ do
|
||||||
then return $ errResponse status416 "HTTP Range error"
|
|
||||||
else do
|
|
||||||
row <- H.query () stm
|
row <- H.query () stm
|
||||||
let (tableTotal, queryTotal, _ , body) = row
|
let (tableTotal, queryTotal, _ , body) = row
|
||||||
if singular
|
if singular
|
||||||
@@ -85,10 +101,7 @@ app dbStructure conf reqBody req =
|
|||||||
then responseLBS status404 [] ""
|
then responseLBS status404 [] ""
|
||||||
else responseLBS status200 [contentTypeH] (cs body)
|
else responseLBS status200 [contentTypeH] (cs body)
|
||||||
else do
|
else do
|
||||||
let frm = rangeOffset range
|
let (status, contentRange) = rangeHeader queryTotal tableTotal
|
||||||
to = frm + toInteger queryTotal - 1
|
|
||||||
contentRange = contentRangeH frm to (toInteger <$> tableTotal)
|
|
||||||
status = rangeStatus frm to (toInteger <$> tableTotal)
|
|
||||||
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
|
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
|
||||||
. sortBy (comparing fst)
|
. sortBy (comparing fst)
|
||||||
. map (join (***) cs)
|
. map (join (***) cs)
|
||||||
@@ -165,14 +178,16 @@ app dbStructure conf reqBody req =
|
|||||||
then do
|
then do
|
||||||
let p = V.head payload
|
let p = V.head payload
|
||||||
jwtSecret = configJwtSecret conf
|
jwtSecret = configJwtSecret conf
|
||||||
|
respondToRange $ do
|
||||||
bodyJson <- H.query () (callProc qi p)
|
row <- H.query () (callProc qi p range shouldCount)
|
||||||
returnJWT <- H.query qi doesProcReturnJWT
|
returnJWT <- H.query qi doesProcReturnJWT
|
||||||
return $ responseLBS status200 [jsonH]
|
let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row
|
||||||
(let body = fromMaybe emptyArray bodyJson in
|
(status, contentRange) = rangeHeader queryTotal tableTotal
|
||||||
if returnJWT
|
in
|
||||||
then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}"
|
return $ responseLBS status [jsonH, contentRange]
|
||||||
else cs $ encode body)
|
(if returnJWT
|
||||||
|
then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}"
|
||||||
|
else cs $ encode body)
|
||||||
else return notFound
|
else return notFound
|
||||||
|
|
||||||
(ActionRead, TargetRoot, Nothing) -> do
|
(ActionRead, TargetRoot, Nothing) -> do
|
||||||
@@ -196,6 +211,8 @@ app dbStructure conf reqBody req =
|
|||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||||
schema = cs $ configSchema conf
|
schema = cs $ configSchema conf
|
||||||
apiRequest = userApiRequest schema req reqBody
|
apiRequest = userApiRequest schema req reqBody
|
||||||
|
shouldCount = iPreferCount apiRequest
|
||||||
|
range = restrictRange (configMaxRows conf) $ iRange apiRequest
|
||||||
readDbRequest = DbRead <$> buildReadRequest (dbRelations dbStructure) apiRequest
|
readDbRequest = DbRead <$> buildReadRequest (dbRelations dbStructure) apiRequest
|
||||||
mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest
|
mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest
|
||||||
selectQuery = requestToQuery schema <$> readDbRequest
|
selectQuery = requestToQuery schema <$> readDbRequest
|
||||||
@@ -203,6 +220,14 @@ app dbStructure conf reqBody req =
|
|||||||
mutateQuery = requestToQuery schema <$> mutateDbRequest
|
mutateQuery = requestToQuery schema <$> mutateDbRequest
|
||||||
readSqlParts = (,) <$> selectQuery <*> countQuery
|
readSqlParts = (,) <$> selectQuery <*> countQuery
|
||||||
mutateSqlParts = (,) <$> selectQuery <*> mutateQuery
|
mutateSqlParts = (,) <$> selectQuery <*> mutateQuery
|
||||||
|
respondToRange response = if range == emptyRange
|
||||||
|
then return $ errResponse status416 "HTTP Range error"
|
||||||
|
else response
|
||||||
|
rangeHeader queryTotal tableTotal = let frm = rangeOffset range
|
||||||
|
to = frm + toInteger queryTotal - 1
|
||||||
|
contentRange = contentRangeH frm to (toInteger <$> tableTotal)
|
||||||
|
status = rangeStatus frm to (toInteger <$> tableTotal)
|
||||||
|
in (status, contentRange)
|
||||||
|
|
||||||
rangeStatus :: Integer -> Integer -> Maybe Integer -> Status
|
rangeStatus :: Integer -> Integer -> Maybe Integer -> Status
|
||||||
rangeStatus _ _ Nothing = status200
|
rangeStatus _ _ Nothing = status200
|
||||||
@@ -224,7 +249,7 @@ contentRangeH frm to total =
|
|||||||
fromInRange = frm <= to
|
fromInRange = frm <= to
|
||||||
|
|
||||||
jsonH :: Header
|
jsonH :: Header
|
||||||
jsonH = (hContentType, "application/json")
|
jsonH = (hContentType, "application/json; charset=utf-8")
|
||||||
|
|
||||||
formatRelationError :: Text -> Text
|
formatRelationError :: Text -> Text
|
||||||
formatRelationError = formatGeneralError
|
formatRelationError = formatGeneralError
|
||||||
|
|||||||
+32
-30
@@ -18,19 +18,20 @@ module PostgREST.Auth (
|
|||||||
, tokenJWT
|
, tokenJWT
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad (join)
|
import Control.Lens
|
||||||
import Data.Aeson (Value (..), Object)
|
import Data.Aeson (Value (..), parseJSON, toJSON)
|
||||||
import Data.Aeson.Types (emptyObject, emptyArray)
|
import Data.Aeson.Lens
|
||||||
|
import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray)
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import Data.Vector as V (null, head)
|
import qualified Data.Vector as V
|
||||||
import Data.Map as M (fromList, toList)
|
import qualified Data.HashMap.Strict as M
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Time.Clock (NominalDiffTime)
|
import Data.Time.Clock (NominalDiffTime)
|
||||||
import PostgREST.QueryBuilder (pgFmtLit, pgFmtIdent, unquoted)
|
import PostgREST.QueryBuilder (pgFmtIdent, pgFmtLit, unquoted)
|
||||||
import qualified Web.JWT as JWT
|
import qualified Web.JWT as JWT
|
||||||
import qualified Data.HashMap.Lazy as H
|
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Receives a map of JWT claims and returns a list
|
Receives a map of JWT claims and returns a list
|
||||||
@@ -39,12 +40,12 @@ import qualified Data.HashMap.Lazy as H
|
|||||||
this one is mapped to a SET ROLE statement.
|
this one is mapped to a SET ROLE statement.
|
||||||
In case there is any problem decoding the JWT it returns Nothing.
|
In case there is any problem decoding the JWT it returns Nothing.
|
||||||
-}
|
-}
|
||||||
claimsToSQL :: JWT.ClaimsMap -> [BS.ByteString]
|
claimsToSQL :: M.HashMap Text Value -> [BS.ByteString]
|
||||||
claimsToSQL = map setVar . toList
|
claimsToSQL = map setVar . M.toList
|
||||||
where
|
where
|
||||||
setVar ("role", String val) = setRole val
|
setVar ("role", String val) = setRole val
|
||||||
setVar (k, val) = "set local postgrest.claims." <> cs (pgFmtIdent k) <>
|
setVar (k, val) = "set local " <> cs (pgFmtIdent $ "postgrest.claims." <> k)
|
||||||
" = " <> cs (valueToVariable val) <> ";"
|
<> " = " <> cs (valueToVariable val) <> ";"
|
||||||
valueToVariable = pgFmtLit . unquoted
|
valueToVariable = pgFmtLit . unquoted
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
@@ -52,19 +53,22 @@ claimsToSQL = map setVar . toList
|
|||||||
returns a map of JWT claims
|
returns a map of JWT claims
|
||||||
In case there is any problem decoding the JWT it returns Nothing.
|
In case there is any problem decoding the JWT it returns Nothing.
|
||||||
-}
|
-}
|
||||||
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Maybe JWT.ClaimsMap
|
|
||||||
|
|
||||||
|
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Either Text (M.HashMap Text Value)
|
||||||
jwtClaims secret input time =
|
jwtClaims secret input time =
|
||||||
case join $ claim JWT.exp of
|
case mClaims of
|
||||||
Just expires ->
|
Nothing -> Right M.empty
|
||||||
if JWT.secondsSinceEpoch expires > time
|
Just claims -> do
|
||||||
then customClaims
|
let mExp = claims ^? key "exp" . _Integer
|
||||||
else Nothing
|
expired = fromMaybe False $ (<= time) . fromInteger <$> mExp
|
||||||
_ -> customClaims
|
if expired
|
||||||
where
|
then Left "JWT expired"
|
||||||
decoded = JWT.decodeAndVerifySignature secret input
|
else Right (value2map claims)
|
||||||
claim :: (JWT.JWTClaimsSet -> a) -> Maybe a
|
where
|
||||||
claim prop = prop . JWT.claims <$> decoded
|
mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature secret input
|
||||||
customClaims = claim JWT.unregisteredClaims
|
value2map (Object o) = o
|
||||||
|
value2map _ = M.empty
|
||||||
|
|
||||||
{-| Receives the name of a role and returns a SET ROLE statement -}
|
{-| Receives the name of a role and returns a SET ROLE statement -}
|
||||||
setRole :: Text -> BS.ByteString
|
setRole :: Text -> BS.ByteString
|
||||||
@@ -76,10 +80,8 @@ setRole r = "set local role " <> cs (pgFmtLit r) <> ";"
|
|||||||
and returns a signed JWT.
|
and returns a signed JWT.
|
||||||
-}
|
-}
|
||||||
tokenJWT :: JWT.Secret -> Value -> Text
|
tokenJWT :: JWT.Secret -> Value -> Text
|
||||||
tokenJWT secret (Array a) = JWT.encodeSigned JWT.HS256 secret
|
tokenJWT secret (Array arr) =
|
||||||
JWT.def { JWT.unregisteredClaims = fromHashMap o }
|
let obj = if V.null arr then emptyObject else V.head arr
|
||||||
where
|
jcs = parseMaybe parseJSON obj :: Maybe JWT.JWTClaimsSet in
|
||||||
Object o = if V.null a then emptyObject else V.head a
|
JWT.encodeSigned JWT.HS256 secret $ fromMaybe JWT.def jcs
|
||||||
fromHashMap :: Object -> JWT.ClaimsMap
|
tokenJWT secret _ = tokenJWT secret emptyArray
|
||||||
fromHashMap = M.fromList . H.toList
|
|
||||||
tokenJWT secret _ = tokenJWT secret emptyArray
|
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ import Network.Wai
|
|||||||
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
|
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
|
||||||
import Options.Applicative
|
import Options.Applicative
|
||||||
import Paths_postgrest (version)
|
import Paths_postgrest (version)
|
||||||
|
import Prelude
|
||||||
import Safe (readMay)
|
import Safe (readMay)
|
||||||
import Web.JWT (Secret, secret)
|
import Web.JWT (Secret, secret)
|
||||||
import Prelude
|
|
||||||
|
|
||||||
-- | Data type to store all command line options
|
-- | Data type to store all command line options
|
||||||
data AppConfig = AppConfig {
|
data AppConfig = AppConfig {
|
||||||
@@ -43,6 +43,7 @@ data AppConfig = AppConfig {
|
|||||||
, configJwtSecret :: Secret
|
, configJwtSecret :: Secret
|
||||||
, configPool :: Int
|
, configPool :: Int
|
||||||
, configMaxRows :: Maybe Integer
|
, configMaxRows :: Maybe Integer
|
||||||
|
, configQuiet :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
argParser :: Parser AppConfig
|
argParser :: Parser AppConfig
|
||||||
@@ -55,6 +56,7 @@ argParser = AppConfig
|
|||||||
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
|
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
|
||||||
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
|
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
|
||||||
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
|
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
|
||||||
|
<*> pure False
|
||||||
|
|
||||||
defaultCorsPolicy :: CorsResourcePolicy
|
defaultCorsPolicy :: CorsResourcePolicy
|
||||||
defaultCorsPolicy = CorsResourcePolicy Nothing
|
defaultCorsPolicy = CorsResourcePolicy Nothing
|
||||||
|
|||||||
@@ -10,23 +10,25 @@ module PostgREST.DbStructure (
|
|||||||
, doesProcReturnJWT
|
, doesProcReturnJWT
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Hasql.Query as H
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Query as H
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Control.Monad (join, replicateM)
|
import Control.Monad (join, replicateM)
|
||||||
import Data.Functor.Contravariant (contramap)
|
import Data.Functor.Contravariant (contramap)
|
||||||
import Text.InterpolatedString.Perl6 (q)
|
import Data.List (elemIndex, find, sort,
|
||||||
import Data.List (elemIndex, find, subsequences, sort, transpose)
|
subsequences, transpose)
|
||||||
import Data.Maybe (fromMaybe, fromJust, isJust, mapMaybe, listToMaybe)
|
import Data.Maybe (fromJust, fromMaybe, isJust,
|
||||||
|
listToMaybe, mapMaybe)
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import Data.Text (Text, split)
|
import Data.Text (Text, split)
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Session as H
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
|
import Text.InterpolatedString.Perl6 (q)
|
||||||
|
|
||||||
import GHC.Exts (groupWith)
|
import Data.Int (Int32)
|
||||||
import Data.Int (Int32)
|
import GHC.Exts (groupWith)
|
||||||
import Prelude
|
import Prelude
|
||||||
|
|
||||||
getDbStructure :: Schema -> H.Session DbStructure
|
getDbStructure :: Schema -> H.Session DbStructure
|
||||||
@@ -556,69 +558,76 @@ allSynonyms :: [Column] -> H.Query () [(Column,Column)]
|
|||||||
allSynonyms cols =
|
allSynonyms cols =
|
||||||
H.statement sql HE.unit (decodeSynonyms cols) True
|
H.statement sql HE.unit (decodeSynonyms cols) True
|
||||||
where
|
where
|
||||||
|
-- query explanation at https://gist.github.com/ruslantalpa/2eab8c930a65e8043d8f
|
||||||
sql = [q|
|
sql = [q|
|
||||||
WITH synonyms AS (
|
WITH view_columns AS (
|
||||||
/*
|
SELECT
|
||||||
-- CTE to replace the view from information_schema because the information in it depended on the logged in role
|
c.oid AS view_oid,
|
||||||
-- notice the commented line
|
a.attname::information_schema.sql_identifier AS column_name
|
||||||
*/
|
FROM pg_attribute a
|
||||||
WITH view_column_usage AS (
|
JOIN pg_class c ON a.attrelid = c.oid
|
||||||
SELECT DISTINCT
|
JOIN pg_namespace nc ON c.relnamespace = nc.oid
|
||||||
CAST(current_database() AS character varying) AS view_catalog,
|
WHERE
|
||||||
CAST(nv.nspname AS character varying) AS view_schema,
|
NOT pg_is_other_temp_schema(nc.oid)
|
||||||
CAST(v.relname AS character varying) AS view_name,
|
AND a.attnum > 0
|
||||||
CAST(current_database() AS character varying) AS table_catalog,
|
AND NOT a.attisdropped
|
||||||
CAST(nt.nspname AS character varying) AS table_schema,
|
AND (c.relkind = 'v'::"char")
|
||||||
CAST(t.relname AS character varying) AS table_name,
|
AND nc.nspname NOT IN ('information_schema', 'pg_catalog')
|
||||||
CAST(a.attname AS character varying) AS column_name
|
),
|
||||||
FROM pg_namespace nv, pg_class v, pg_depend dv,
|
view_column_usage AS (
|
||||||
pg_depend dt, pg_class t, pg_namespace nt,
|
SELECT DISTINCT
|
||||||
pg_attribute a
|
v.oid as view_oid,
|
||||||
WHERE nv.oid = v.relnamespace
|
nv.nspname::information_schema.sql_identifier AS view_schema,
|
||||||
AND v.relkind = 'v'
|
v.relname::information_schema.sql_identifier AS view_name,
|
||||||
AND v.oid = dv.refobjid
|
nt.nspname::information_schema.sql_identifier AS table_schema,
|
||||||
AND dv.refclassid = 'pg_catalog.pg_class'::regclass
|
t.relname::information_schema.sql_identifier AS table_name,
|
||||||
AND dv.classid = 'pg_catalog.pg_rewrite'::regclass
|
a.attname::information_schema.sql_identifier AS column_name,
|
||||||
AND dv.deptype = 'i'
|
pg_get_viewdef(v.oid)::information_schema.character_data AS view_definition
|
||||||
AND dv.objid = dt.objid
|
FROM pg_namespace nv
|
||||||
AND dv.refobjid <> dt.refobjid
|
JOIN pg_class v ON nv.oid = v.relnamespace
|
||||||
AND dt.classid = 'pg_catalog.pg_rewrite'::regclass
|
JOIN pg_depend dv ON v.oid = dv.refobjid
|
||||||
AND dt.refclassid = 'pg_catalog.pg_class'::regclass
|
JOIN pg_depend dt ON dv.objid = dt.objid
|
||||||
AND dt.refobjid = t.oid
|
JOIN pg_class t ON dt.refobjid = t.oid
|
||||||
AND t.relnamespace = nt.oid
|
JOIN pg_namespace nt ON t.relnamespace = nt.oid
|
||||||
AND t.relkind IN ('r', 'v', 'f')
|
JOIN pg_attribute a ON t.oid = a.attrelid AND dt.refobjsubid = a.attnum
|
||||||
AND t.oid = a.attrelid
|
|
||||||
AND dt.refobjsubid = a.attnum
|
WHERE
|
||||||
/*--AND pg_has_role(t.relowner, 'USAGE')*/
|
nv.nspname not in ('information_schema', 'pg_catalog')
|
||||||
)
|
AND v.relkind = 'v'::"char"
|
||||||
SELECT
|
AND dv.refclassid = 'pg_class'::regclass::oid
|
||||||
vcu.table_schema AS src_table_schema,
|
AND dv.classid = 'pg_rewrite'::regclass::oid
|
||||||
vcu.table_name AS src_table_name,
|
AND dv.deptype = 'i'::"char"
|
||||||
vcu.column_name AS src_column_name,
|
AND dv.refobjid <> dt.refobjid
|
||||||
view.schemaname AS syn_table_schema,
|
AND dt.classid = 'pg_rewrite'::regclass::oid
|
||||||
view.viewname AS syn_table_name,
|
AND dt.refclassid = 'pg_class'::regclass::oid
|
||||||
view.definition AS view_definition
|
AND (t.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
|
||||||
FROM
|
),
|
||||||
pg_catalog.pg_views AS view,
|
candidates AS (
|
||||||
view_column_usage AS vcu
|
SELECT
|
||||||
WHERE
|
vcu.*,
|
||||||
view.schemaname = vcu.view_schema AND
|
(
|
||||||
view.viewname = vcu.view_name AND
|
SELECT CASE WHEN match IS NOT NULL THEN coalesce(match[7], match[4]) END
|
||||||
view.schemaname NOT IN ('pg_catalog', 'information_schema')
|
FROM REGEXP_MATCHES(
|
||||||
/*--AND (SELECT COUNT(*) FROM information_schema.view_table_usage WHERE view_schema = view.schemaname AND view_name = view.viewname) = 1*/
|
CONCAT('SELECT ', SPLIT_PART(vcu.view_definition, 'SELECT', 2)),
|
||||||
|
CONCAT('SELECT.*?((',vcu.table_name,')|(\w+))\.(', vcu.column_name, ')(\sAS\s(")?([^"]+)\6)?.*?FROM.*?',vcu.table_schema,'\.(\2|',vcu.table_name,'\s+(AS\s)?\3)'),
|
||||||
|
'ns'
|
||||||
|
) match
|
||||||
|
) AS view_column_name
|
||||||
|
FROM view_column_usage AS vcu
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
src_table_schema, src_table_name, src_column_name,
|
c.table_schema,
|
||||||
syn_table_schema, syn_table_name,
|
c.table_name,
|
||||||
(regexp_matches(view_definition, CONCAT('\.(', src_column_name, ')(?=,|$)'), 'gn'))[1] AS syn_column_name
|
c.column_name AS table_column_name,
|
||||||
FROM synonyms
|
c.view_schema,
|
||||||
UNION (
|
c.view_name,
|
||||||
SELECT
|
c.view_column_name
|
||||||
src_table_schema, src_table_name, src_column_name,
|
FROM view_columns AS vc, candidates AS c
|
||||||
syn_table_schema, syn_table_name,
|
WHERE
|
||||||
(regexp_matches(view_definition, CONCAT('\.', src_column_name, '\sAS\s("?)(.+?)\1(,|$)'), 'gn'))[2] AS syn_column_name /* " <- for syntax highlighting */
|
vc.view_oid = c.view_oid AND
|
||||||
FROM synonyms
|
vc.column_name = c.view_column_name
|
||||||
) |]
|
ORDER BY c.view_schema, c.view_name, c.table_name, c.view_column_name
|
||||||
|
|]
|
||||||
|
|
||||||
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column)
|
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column)
|
||||||
synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
|
synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
|
||||||
|
|||||||
+21
-10
@@ -7,10 +7,12 @@ module PostgREST.Error (pgErrResponse, errResponse) where
|
|||||||
|
|
||||||
import Data.Aeson ((.=))
|
import Data.Aeson ((.=))
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import qualified Hasql.Pool as P
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Session as H
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
import qualified Network.HTTP.Types.Status as HT
|
import qualified Network.HTTP.Types.Status as HT
|
||||||
@@ -19,10 +21,17 @@ import Network.Wai (Response, responseLBS)
|
|||||||
errResponse :: HT.Status -> Text -> Response
|
errResponse :: HT.Status -> Text -> Response
|
||||||
errResponse status message = responseLBS status [(hContentType, "application/json")] (cs $ T.concat ["{\"message\":\"",message,"\"}"])
|
errResponse status message = responseLBS status [(hContentType, "application/json")] (cs $ T.concat ["{\"message\":\"",message,"\"}"])
|
||||||
|
|
||||||
pgErrResponse :: H.Error -> Response
|
pgErrResponse :: P.UsageError -> Response
|
||||||
pgErrResponse e = responseLBS (httpStatus e)
|
pgErrResponse e = responseLBS (httpStatus e)
|
||||||
[(hContentType, "application/json")] (JSON.encode e)
|
[(hContentType, "application/json")] (JSON.encode e)
|
||||||
|
|
||||||
|
instance JSON.ToJSON P.UsageError where
|
||||||
|
toJSON (P.ConnectionError e) = JSON.object [
|
||||||
|
"code" .= ("" :: T.Text),
|
||||||
|
"message" .= ("Connection error" :: T.Text),
|
||||||
|
"details" .= (cs (fromMaybe "" e) :: T.Text)]
|
||||||
|
toJSON (P.SessionError e) = JSON.toJSON e -- H.Error
|
||||||
|
|
||||||
instance JSON.ToJSON H.Error where
|
instance JSON.ToJSON H.Error where
|
||||||
toJSON (H.ResultError (H.ServerError c m d h)) = JSON.object [
|
toJSON (H.ResultError (H.ServerError c m d h)) = JSON.object [
|
||||||
"code" .= (cs c::T.Text),
|
"code" .= (cs c::T.Text),
|
||||||
@@ -51,15 +60,17 @@ instance JSON.ToJSON H.Error where
|
|||||||
"message" .= ("Database client error"::String),
|
"message" .= ("Database client error"::String),
|
||||||
"details" .= (fmap cs d::Maybe T.Text)]
|
"details" .= (fmap cs d::Maybe T.Text)]
|
||||||
|
|
||||||
httpStatus :: H.Error -> HT.Status
|
httpStatus :: P.UsageError -> HT.Status
|
||||||
httpStatus (H.ResultError (H.ServerError c _ _ _)) =
|
httpStatus (P.ConnectionError _) =
|
||||||
|
HT.status500
|
||||||
|
httpStatus (P.SessionError (H.ResultError (H.ServerError c _ _ _))) =
|
||||||
case cs c of
|
case cs c of
|
||||||
'0':'8':_ -> HT.status503 -- pg connection err
|
'0':'8':_ -> HT.status503 -- pg connection err
|
||||||
'0':'9':_ -> HT.status500 -- triggered action exception
|
'0':'9':_ -> HT.status500 -- triggered action exception
|
||||||
'0':'L':_ -> HT.status403 -- invalid grantor
|
'0':'L':_ -> HT.status403 -- invalid grantor
|
||||||
'0':'P':_ -> HT.status403 -- invalid role specification
|
'0':'P':_ -> HT.status403 -- invalid role specification
|
||||||
"23503" -> HT.status409 -- foreign_key_violation
|
"23503" -> HT.status409 -- foreign_key_violation
|
||||||
"23505" -> HT.status409 -- unique_violation
|
"23505" -> HT.status409 -- unique_violation
|
||||||
'2':'5':_ -> HT.status500 -- invalid tx state
|
'2':'5':_ -> HT.status500 -- invalid tx state
|
||||||
'2':'8':_ -> HT.status403 -- invalid auth specification
|
'2':'8':_ -> HT.status403 -- invalid auth specification
|
||||||
'2':'D':_ -> HT.status500 -- invalid tx termination
|
'2':'D':_ -> HT.status500 -- invalid tx termination
|
||||||
@@ -76,8 +87,8 @@ httpStatus (H.ResultError (H.ServerError c _ _ _)) =
|
|||||||
'H':'V':_ -> HT.status500 -- foreign data wrapper error
|
'H':'V':_ -> HT.status500 -- foreign data wrapper error
|
||||||
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
|
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
|
||||||
'X':'X':_ -> HT.status500 -- internal Error
|
'X':'X':_ -> HT.status500 -- internal Error
|
||||||
"42P01" -> HT.status404 -- undefined table
|
"42P01" -> HT.status404 -- undefined table
|
||||||
"42501" -> HT.status404 -- insufficient privilege
|
"42501" -> HT.status404 -- insufficient privilege
|
||||||
_ -> HT.status400
|
_ -> HT.status400
|
||||||
httpStatus (H.ResultError _) = HT.status500
|
httpStatus (P.SessionError (H.ResultError _)) = HT.status500
|
||||||
httpStatus (H.ClientError _) = HT.status503
|
httpStatus (P.SessionError (H.ClientError _)) = HT.status503
|
||||||
|
|||||||
+21
-47
@@ -9,29 +9,20 @@ import PostgREST.Config (AppConfig (..),
|
|||||||
prettyVersion,
|
prettyVersion,
|
||||||
readOptions)
|
readOptions)
|
||||||
import PostgREST.DbStructure
|
import PostgREST.DbStructure
|
||||||
import PostgREST.Error (errResponse, pgErrResponse)
|
|
||||||
import PostgREST.Middleware
|
|
||||||
import PostgREST.QueryBuilder (inTransaction, Isolation(..))
|
|
||||||
|
|
||||||
import Control.Monad (unless, void)
|
import Control.Monad
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
import Data.Pool
|
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
|
||||||
import qualified Hasql.Query as H
|
import qualified Hasql.Query as H
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Session as H
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
import qualified Network.HTTP.Types.Status as HT
|
import qualified Hasql.Pool as P
|
||||||
import Network.Wai
|
|
||||||
import Network.Wai.Handler.Warp
|
import Network.Wai.Handler.Warp
|
||||||
import Network.Wai.Middleware.RequestLogger (logStdout)
|
|
||||||
import System.IO (BufferMode (..),
|
import System.IO (BufferMode (..),
|
||||||
hSetBuffering, stderr,
|
hSetBuffering, stderr,
|
||||||
stdin, stdout)
|
stdin, stdout)
|
||||||
import Web.JWT (secret)
|
import Web.JWT (secret)
|
||||||
|
|
||||||
#ifndef mingw32_HOST_OS
|
#ifndef mingw32_HOST_OS
|
||||||
import System.Posix.Signals
|
import System.Posix.Signals
|
||||||
import Control.Concurrent (myThreadId)
|
import Control.Concurrent (myThreadId)
|
||||||
@@ -55,50 +46,33 @@ main = do
|
|||||||
|
|
||||||
conf <- readOptions
|
conf <- readOptions
|
||||||
let port = configPort conf
|
let port = configPort conf
|
||||||
|
pgSettings = cs (configDatabase conf)
|
||||||
|
appSettings = setPort port
|
||||||
|
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
||||||
|
$ defaultSettings
|
||||||
|
|
||||||
unless (secret "secret" /= configJwtSecret conf) $
|
unless (secret "secret" /= configJwtSecret conf) $
|
||||||
putStrLn "WARNING, running in insecure mode, JWT secret is the default value"
|
putStrLn "WARNING, running in insecure mode, JWT secret is the default value"
|
||||||
Prelude.putStrLn $ "Listening on port " ++
|
Prelude.putStrLn $ "Listening on port " ++
|
||||||
(show $ configPort conf :: String)
|
(show $ configPort conf :: String)
|
||||||
|
|
||||||
let pgSettings = cs (configDatabase conf)
|
pool <- P.acquire (configPool conf, 10, pgSettings)
|
||||||
appSettings = setPort port
|
|
||||||
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
|
||||||
$ defaultSettings
|
|
||||||
middle = logStdout . defaultMiddle
|
|
||||||
|
|
||||||
pool <- createPool (H.acquire pgSettings)
|
|
||||||
(either (const $ return ()) H.release) 1 1 (configPool conf)
|
|
||||||
|
|
||||||
dbStructure <- withResource pool $ \case
|
|
||||||
Left err -> error $ show err
|
|
||||||
Right c -> do
|
|
||||||
supported <- H.run isServerVersionSupported c
|
|
||||||
case supported of
|
|
||||||
Left e -> error $ show e
|
|
||||||
Right good -> unless good $
|
|
||||||
error (
|
|
||||||
"Cannot run in this PostgreSQL version, PostgREST needs at least "
|
|
||||||
<> show minimumPgVersion)
|
|
||||||
|
|
||||||
dbOrError <- H.run (getDbStructure (cs $ configSchema conf)) c
|
|
||||||
either (error . show) return dbOrError
|
|
||||||
|
|
||||||
#ifndef mingw32_HOST_OS
|
#ifndef mingw32_HOST_OS
|
||||||
tid <- myThreadId
|
tid <- myThreadId
|
||||||
void $ installHandler keyboardSignal (Catch $ do
|
forM_ [sigINT, sigTERM] $ \sig ->
|
||||||
destroyAllResources pool
|
void $ installHandler sig (Catch $ do
|
||||||
throwTo tid UserInterrupt
|
P.release pool
|
||||||
) Nothing
|
throwTo tid UserInterrupt
|
||||||
|
) Nothing
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
runSettings appSettings $ middle $ \ req respond -> do
|
result <- P.use pool $ do
|
||||||
time <- getPOSIXTime
|
supported <- isServerVersionSupported
|
||||||
body <- strictRequestBody req
|
unless supported $ error (
|
||||||
let handleReq = H.run $ inTransaction ReadCommitted
|
"Cannot run in this PostgreSQL version, PostgREST needs at least "
|
||||||
(runWithClaims conf time (app dbStructure conf body) req)
|
<> show minimumPgVersion)
|
||||||
withResource pool $ \case
|
getDbStructure (cs $ configSchema conf)
|
||||||
Left err -> respond $ errResponse HT.status500 (cs . show $ err)
|
|
||||||
Right c -> do
|
let dbStructure = either (error.show) id result
|
||||||
resOrError <- handleReq c
|
runSettings appSettings $ postgrest conf dbStructure pool
|
||||||
either (respond . pgErrResponse) respond resOrError
|
|
||||||
|
|||||||
+26
-23
@@ -3,52 +3,55 @@
|
|||||||
|
|
||||||
module PostgREST.Middleware where
|
module PostgREST.Middleware where
|
||||||
|
|
||||||
|
import Control.Monad (unless)
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Text
|
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
|
import Data.Text
|
||||||
import Data.Time.Clock (NominalDiffTime)
|
import Data.Time.Clock (NominalDiffTime)
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Transaction as H
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (hAccept, hAuthorization)
|
import Network.HTTP.Types.Header (hAccept, hAuthorization)
|
||||||
import Network.HTTP.Types.Status (status415, status400)
|
import Network.HTTP.Types.Status (status400, status415)
|
||||||
import Network.Wai (Application, Request (..), Response,
|
import Network.Wai (Application, Request (..),
|
||||||
requestHeaders)
|
Response, requestHeaders)
|
||||||
import Network.Wai.Middleware.Cors (cors)
|
import Network.Wai.Middleware.Cors (cors)
|
||||||
import Network.Wai.Middleware.Gzip (def, gzip)
|
import Network.Wai.Middleware.Gzip (def, gzip)
|
||||||
import Network.Wai.Middleware.Static (only, staticPolicy)
|
import Network.Wai.Middleware.Static (only, staticPolicy)
|
||||||
|
|
||||||
import PostgREST.ApiRequest (pickContentType)
|
import PostgREST.ApiRequest (pickContentType)
|
||||||
import PostgREST.Auth (setRole, jwtClaims, claimsToSQL)
|
import PostgREST.Auth (setRole, jwtClaims, claimsToSQL)
|
||||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||||
import PostgREST.Error (errResponse)
|
import PostgREST.Error (errResponse)
|
||||||
|
|
||||||
import Prelude hiding(concat)
|
import Prelude hiding (concat, null)
|
||||||
|
|
||||||
import qualified Data.Map.Lazy as M
|
|
||||||
|
|
||||||
runWithClaims :: AppConfig -> NominalDiffTime ->
|
runWithClaims :: AppConfig -> NominalDiffTime ->
|
||||||
(Request -> H.Session Response) ->
|
(Request -> H.Transaction Response) ->
|
||||||
Request -> H.Session Response
|
Request -> H.Transaction Response
|
||||||
runWithClaims conf time app req = do
|
runWithClaims conf time app req = do
|
||||||
H.sql setAnon
|
H.sql setAnon
|
||||||
case split (== ' ') (cs auth) of
|
let tokenStr = case split (== ' ') (cs auth) of
|
||||||
("Bearer" : tokenStr : _) ->
|
("Bearer" : t : _) -> t
|
||||||
case jwtClaims jwtSecret tokenStr time of
|
_ -> ""
|
||||||
Just claims ->
|
eClaims = jwtClaims jwtSecret tokenStr time
|
||||||
if M.member "role" claims
|
case eClaims of
|
||||||
then do
|
Left e -> clientErr e
|
||||||
mapM_ H.sql $ claimsToSQL claims
|
Right claims ->
|
||||||
app req
|
if M.null claims && not (null tokenStr)
|
||||||
else invalidJWT
|
then clientErr "Invalid JWT"
|
||||||
_ -> invalidJWT
|
else do
|
||||||
_ -> app req
|
let cmdBatch = mconcat $ claimsToSQL claims
|
||||||
|
unless (BS.null cmdBatch) (H.sql cmdBatch)
|
||||||
|
app req
|
||||||
where
|
where
|
||||||
hdrs = requestHeaders req
|
hdrs = requestHeaders req
|
||||||
jwtSecret = configJwtSecret conf
|
jwtSecret = configJwtSecret conf
|
||||||
auth = fromMaybe "" $ lookup hAuthorization hdrs
|
auth = fromMaybe "" $ lookup hAuthorization hdrs
|
||||||
anon = cs $ configAnonRole conf
|
anon = cs $ configAnonRole conf
|
||||||
setAnon = setRole anon
|
setAnon = setRole anon
|
||||||
invalidJWT = return $ errResponse status400 "Invalid JWT"
|
clientErr = return . errResponse status400
|
||||||
|
|
||||||
unsupportedAccept :: Application -> Application
|
unsupportedAccept :: Application -> Application
|
||||||
unsupportedAccept app req respond =
|
unsupportedAccept app req respond =
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ module PostgREST.Parsers
|
|||||||
-- )
|
-- )
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Applicative hiding ((<$>))
|
import Control.Applicative hiding ((<$>))
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
|
import PostgREST.QueryBuilder (operators)
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
import Text.ParserCombinators.Parsec hiding (many, (<|>))
|
import Text.ParserCombinators.Parsec hiding (many, (<|>))
|
||||||
import PostgREST.QueryBuilder (operators)
|
|
||||||
|
|
||||||
pRequestSelect :: Text -> Parser ReadRequest
|
pRequestSelect :: Text -> Parser ReadRequest
|
||||||
pRequestSelect rootNodeName = do
|
pRequestSelect rootNodeName = do
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ module PostgREST.QueryBuilder (
|
|||||||
, callProc
|
, callProc
|
||||||
, createReadStatement
|
, createReadStatement
|
||||||
, createWriteStatement
|
, createWriteStatement
|
||||||
, inTransaction
|
|
||||||
, operators
|
, operators
|
||||||
, pgFmtIdent
|
, pgFmtIdent
|
||||||
, pgFmtLit
|
, pgFmtLit
|
||||||
@@ -27,11 +26,9 @@ module PostgREST.QueryBuilder (
|
|||||||
, sourceCTEName
|
, sourceCTEName
|
||||||
, unquoted
|
, unquoted
|
||||||
, ResultsWithCount
|
, ResultsWithCount
|
||||||
, Isolation(..)
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Hasql.Query as H
|
import qualified Hasql.Query as H
|
||||||
import qualified Hasql.Session as H
|
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
|
|
||||||
@@ -46,6 +43,7 @@ import Data.List (find, (\\))
|
|||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
import Data.Text (Text, intercalate, unwords, replace, isInfixOf, toLower, split)
|
import Data.Text (Text, intercalate, unwords, replace, isInfixOf, toLower, split)
|
||||||
import qualified Data.Text as T (map, takeWhile)
|
import qualified Data.Text as T (map, takeWhile)
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Control.Applicative ((<|>))
|
import Control.Applicative ((<|>))
|
||||||
import Control.Monad (join)
|
import Control.Monad (join)
|
||||||
@@ -96,7 +94,7 @@ encodeUniformObjs =
|
|||||||
createReadStatement :: SqlQuery -> SqlQuery -> NonnegRange -> Bool -> Bool -> Bool ->
|
createReadStatement :: SqlQuery -> SqlQuery -> NonnegRange -> Bool -> Bool -> Bool ->
|
||||||
H.Query () ResultsWithCount
|
H.Query () ResultsWithCount
|
||||||
createReadStatement selectQuery countQuery range isSingle countTotal asCsv =
|
createReadStatement selectQuery countQuery range isSingle countTotal asCsv =
|
||||||
H.statement sql HE.unit decodeStandard True
|
unicodeStatement sql HE.unit decodeStandard True
|
||||||
where
|
where
|
||||||
sql = [qc|
|
sql = [qc|
|
||||||
WITH {sourceCTEName} AS ({selectQuery}) SELECT {cols}
|
WITH {sourceCTEName} AS ({selectQuery}) SELECT {cols}
|
||||||
@@ -119,7 +117,7 @@ createWriteStatement :: QualifiedIdentifier -> SqlQuery -> SqlQuery -> Bool ->
|
|||||||
createWriteStatement _ _ _ _ _ _ _ (PayloadParseError _) = undefined
|
createWriteStatement _ _ _ _ _ _ _ (PayloadParseError _) = undefined
|
||||||
createWriteStatement _ _ mutateQuery _ None
|
createWriteStatement _ _ mutateQuery _ None
|
||||||
_ _ (PayloadJSON (UniformObjects _)) =
|
_ _ (PayloadJSON (UniformObjects _)) =
|
||||||
H.statement sql encodeUniformObjs decodeStandardMay True
|
unicodeStatement sql encodeUniformObjs decodeStandardMay True
|
||||||
where
|
where
|
||||||
sql = [qc|
|
sql = [qc|
|
||||||
WITH {sourceCTEName} AS ({mutateQuery})
|
WITH {sourceCTEName} AS ({mutateQuery})
|
||||||
@@ -127,7 +125,7 @@ createWriteStatement _ _ mutateQuery _ None
|
|||||||
|
|
||||||
createWriteStatement qi _ mutateQuery isSingle HeadersOnly
|
createWriteStatement qi _ mutateQuery isSingle HeadersOnly
|
||||||
pKeys _ (PayloadJSON (UniformObjects _)) =
|
pKeys _ (PayloadJSON (UniformObjects _)) =
|
||||||
H.statement sql encodeUniformObjs decodeStandardMay True
|
unicodeStatement sql encodeUniformObjs decodeStandardMay True
|
||||||
where
|
where
|
||||||
sql = [qc|
|
sql = [qc|
|
||||||
WITH {sourceCTEName} AS ({mutateQuery} RETURNING {fromQi qi}.*)
|
WITH {sourceCTEName} AS ({mutateQuery} RETURNING {fromQi qi}.*)
|
||||||
@@ -142,7 +140,7 @@ createWriteStatement qi _ mutateQuery isSingle HeadersOnly
|
|||||||
|
|
||||||
createWriteStatement qi selectQuery mutateQuery isSingle Full
|
createWriteStatement qi selectQuery mutateQuery isSingle Full
|
||||||
pKeys asCsv (PayloadJSON (UniformObjects _)) =
|
pKeys asCsv (PayloadJSON (UniformObjects _)) =
|
||||||
H.statement sql encodeUniformObjs decodeStandardMay True
|
unicodeStatement sql encodeUniformObjs decodeStandardMay True
|
||||||
where
|
where
|
||||||
sql = [qc|
|
sql = [qc|
|
||||||
WITH {sourceCTEName} AS ({mutateQuery} RETURNING {fromQi qi}.*)
|
WITH {sourceCTEName} AS ({mutateQuery} RETURNING {fromQi qi}.*)
|
||||||
@@ -206,18 +204,30 @@ addJoinConditions schema (Node (query, (n, r)) forest) =
|
|||||||
updatedForest = mapM (addJoinConditions schema) forest
|
updatedForest = mapM (addJoinConditions schema) forest
|
||||||
addCond query' con = query'{flt_=con ++ flt_ query'}
|
addCond query' con = query'{flt_=con ++ flt_ query'}
|
||||||
|
|
||||||
callProc :: QualifiedIdentifier -> JSON.Object -> H.Query () (Maybe JSON.Value)
|
type ProcResults = (Maybe Int64, Int64, JSON.Value)
|
||||||
callProc qi params =
|
callProc :: QualifiedIdentifier -> JSON.Object -> NonnegRange -> Bool -> H.Query () (Maybe ProcResults)
|
||||||
H.statement sql HE.unit decodeObj True
|
callProc qi params range countTotal =
|
||||||
|
unicodeStatement sql HE.unit decodeProc True
|
||||||
where
|
where
|
||||||
sql = [qc| SELECT array_to_json(
|
sql = [qc|
|
||||||
coalesce(array_agg(row_to_json(t)), '\{}')
|
WITH t AS (select * {_callSql})
|
||||||
|
SELECT
|
||||||
|
{_countExpr} as countTotal,
|
||||||
|
pg_catalog.count(1) as countResult,
|
||||||
|
array_to_json(
|
||||||
|
coalesce(array_agg(row_to_json(r)), '\{}')
|
||||||
)::character varying
|
)::character varying
|
||||||
from ({_callSql}) t |]
|
FROM (select * from t {limitF range}) r;
|
||||||
|
|]
|
||||||
_args = intercalate "," $ map _assignment (HM.toList params)
|
_args = intercalate "," $ map _assignment (HM.toList params)
|
||||||
_assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
|
_assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
|
||||||
_callSql = [qc| select * from {fromQi qi}({_args}) |] :: BS.ByteString
|
_callSql = [qc| from {fromQi qi}({_args}) |] :: Text
|
||||||
decodeObj = HD.maybeRow (HD.value HD.json)
|
_countExpr = if countTotal
|
||||||
|
then "(select pg_catalog.count(1) from t)"
|
||||||
|
else "null::bigint" :: Text
|
||||||
|
decodeProc = HD.maybeRow procRow
|
||||||
|
procRow = (,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
|
||||||
|
<*> HD.value HD.json
|
||||||
|
|
||||||
operators :: [(Text, SqlFragment)]
|
operators :: [(Text, SqlFragment)]
|
||||||
operators = [
|
operators = [
|
||||||
@@ -430,6 +440,9 @@ getJoinConditions (Relation t cols ft fcs typ lt lc1 lc2) =
|
|||||||
toFilter :: Text -> Text -> Column -> Column -> Filter
|
toFilter :: Text -> Text -> Column -> Column -> Filter
|
||||||
toFilter tb ftb c fc = Filter (colName c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}}))
|
toFilter tb ftb c fc = Filter (colName c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}}))
|
||||||
|
|
||||||
|
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b
|
||||||
|
unicodeStatement = H.statement . T.encodeUtf8
|
||||||
|
|
||||||
emptyOnNull :: Text -> [a] -> Text
|
emptyOnNull :: Text -> [a] -> Text
|
||||||
emptyOnNull val x = if null x then "" else val
|
emptyOnNull val x = if null x then "" else val
|
||||||
|
|
||||||
@@ -504,20 +517,3 @@ pgFmtAsJsonPath (Just xx) = " AS " <> last xx
|
|||||||
|
|
||||||
trimNullChars :: Text -> Text
|
trimNullChars :: Text -> Text
|
||||||
trimNullChars = T.takeWhile (/= '\x0')
|
trimNullChars = T.takeWhile (/= '\x0')
|
||||||
|
|
||||||
data Isolation = ReadCommitted | RepeatableRead | Serializable
|
|
||||||
|
|
||||||
{- |
|
|
||||||
Wrap a session in a transaction of desired isolation level
|
|
||||||
-}
|
|
||||||
inTransaction :: Isolation -> H.Session a -> H.Session a
|
|
||||||
inTransaction lvl f = do
|
|
||||||
H.sql $ "begin " <> isolate <> ";"
|
|
||||||
r <- f
|
|
||||||
H.sql "commit;"
|
|
||||||
return r
|
|
||||||
where
|
|
||||||
isolate = case lvl of
|
|
||||||
ReadCommitted -> "ISOLATION LEVEL READ COMMITTED"
|
|
||||||
RepeatableRead -> "ISOLATION LEVEL REPEATABLE READ"
|
|
||||||
Serializable -> "ISOLATION LEVEL SERIALIZABLE"
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
module PostgREST.Types where
|
module PostgREST.Types where
|
||||||
import Data.Text
|
import Data.Aeson
|
||||||
import Data.Tree
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import Data.Int (Int32)
|
||||||
|
import Data.Text
|
||||||
|
import Data.Tree
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import Data.Aeson
|
|
||||||
import Data.Int (Int32)
|
|
||||||
|
|
||||||
data DbStructure = DbStructure {
|
data DbStructure = DbStructure {
|
||||||
dbTables :: [Table]
|
dbTables :: [Table]
|
||||||
, dbColumns :: [Column]
|
, dbColumns :: [Column]
|
||||||
, dbRelations :: [Relation]
|
, dbRelations :: [Relation]
|
||||||
, dbPrimaryKeys :: [PrimaryKey]
|
, dbPrimaryKeys :: [PrimaryKey]
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -1,8 +1,13 @@
|
|||||||
resolver: lts-5.0
|
resolver: lts-5.5
|
||||||
extra-deps:
|
extra-deps:
|
||||||
- hasql-0.19.3.3
|
|
||||||
- Ranged-sets-0.3.0
|
- Ranged-sets-0.3.0
|
||||||
|
- bytestring-tree-builder-0.2.5
|
||||||
|
- hasql-0.19.9
|
||||||
|
- hasql-pool-0.4
|
||||||
|
- hasql-transaction-0.4.3
|
||||||
- packdeps-0.4.2.1
|
- packdeps-0.4.2.1
|
||||||
|
- postgresql-error-codes-1
|
||||||
|
- postgresql-binary-0.8.1
|
||||||
ghc-options:
|
ghc-options:
|
||||||
postgrest: -O2 -Werror -Wall -fwarn-monomorphism-restriction -fwarn-missing-exported-sigs -fwarn-identities
|
postgrest: -O2 -Werror -Wall -fwarn-monomorphism-restriction -fwarn-missing-exported-sigs -fwarn-identities
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
import PostgREST.Types (DbStructure(..))
|
import Network.Wai (Application)
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
spec :: DbStructure -> H.Connection -> Spec
|
spec :: SpecWith Application
|
||||||
spec struct c = around (withApp cfgDefault struct c)
|
spec = describe "authorization" $ do
|
||||||
$ describe "authorization" $ do
|
|
||||||
|
|
||||||
it "hides tables that anonymous does not own" $
|
it "hides tables that anonymous does not own" $
|
||||||
get "/authors_only" `shouldRespondWith` 404
|
get "/authors_only" `shouldRespondWith` 404
|
||||||
@@ -23,9 +21,25 @@ spec struct c = around (withApp cfgDefault struct c)
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
|
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "application/json"]
|
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "sql functions can encode custom and standard claims" $
|
||||||
|
post "/rpc/jwt_test" "{}"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6MTMwMDgxOTM4MCwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3QiLCJpYXQiOjEzMDA4MTkzODAsImF1ZCI6ImV2ZXJ5b25lIn0._tQCF79-ZZGMlLktd3csM_bVaiMg7A8YvIb6K2hcu5w"} |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "sql functions can read custom and standard claims variables" $ do
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwLCJhdWQiOiJldmVyeW9uZSJ9.AQmCA7CMScvfaDRMqRPeUY6eNf--69gpW-kxaWfq9X0"
|
||||||
|
request methodPost "/rpc/reveal_big_jwt" [auth] "{}"
|
||||||
|
`shouldRespondWith` [json| [
|
||||||
|
{"sub":"fun", "jti":"foo", "nbf":1300819380, "exp":9999999999,
|
||||||
|
"http://postgrest.com/foo":true, "iss":"joe", "iat":1300819380,
|
||||||
|
"aud":"everyone"}] |]
|
||||||
|
|
||||||
it "allows users with permissions to see their tables" $ do
|
it "allows users with permissions to see their tables" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
||||||
request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, UndecidableInstances #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
module Feature.ConcurrentSpec where
|
||||||
|
|
||||||
|
import Control.Monad (void)
|
||||||
|
import Control.Monad.Base
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Control
|
||||||
|
import Control.Concurrent.Async (mapConcurrently)
|
||||||
|
|
||||||
|
import Test.Hspec hiding (pendingWith)
|
||||||
|
import Test.Hspec.Wai.Internal
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Network.Wai.Test (Session)
|
||||||
|
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec =
|
||||||
|
describe "Queryiny in parallel" $
|
||||||
|
it "should not raise 'transaction in progress' error" $
|
||||||
|
raceTest 10 $
|
||||||
|
get "/fakefake"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json|
|
||||||
|
{ "hint": null,
|
||||||
|
"details":null,
|
||||||
|
"code":"42P01",
|
||||||
|
"message":"relation \"test.fakefake\" does not exist"
|
||||||
|
} |]
|
||||||
|
, matchStatus = 404
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
|
raceTest :: Int -> WaiExpectation -> WaiExpectation
|
||||||
|
raceTest times = liftBaseDiscard go
|
||||||
|
where
|
||||||
|
go test = void $ mapConcurrently (const test) [1..times]
|
||||||
|
|
||||||
|
instance MonadBaseControl IO WaiSession where
|
||||||
|
type StM WaiSession a = StM Session a
|
||||||
|
liftBaseWith f = WaiSession $
|
||||||
|
liftBaseWith $ \runInBase ->
|
||||||
|
f $ \k -> runInBase (unWaiSession k)
|
||||||
|
restoreM = WaiSession . restoreM
|
||||||
|
{-# INLINE liftBaseWith #-}
|
||||||
|
{-# INLINE restoreM #-}
|
||||||
|
|
||||||
|
instance MonadBase IO WaiSession where
|
||||||
|
liftBase = liftIO
|
||||||
@@ -5,16 +5,16 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Network.Wai.Test (SResponse(simpleHeaders, simpleBody))
|
import Network.Wai.Test (SResponse(simpleHeaders, simpleBody))
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
import PostgREST.Types (DbStructure(..))
|
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
import Network.Wai (Application)
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
spec :: DbStructure -> H.Connection -> Spec
|
spec :: SpecWith Application
|
||||||
spec struct c = around (withApp cfgDefault struct c) $ describe "CORS" $ do
|
spec =
|
||||||
|
describe "CORS" $ do
|
||||||
let preflightHeaders = [
|
let preflightHeaders = [
|
||||||
("Accept", "*/*"),
|
("Accept", "*/*"),
|
||||||
("Origin", "http://example.com"),
|
("Origin", "http://example.com"),
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
|
|
||||||
import SpecHelper
|
|
||||||
import PostgREST.Types (DbStructure(..))
|
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
spec :: DbStructure -> H.Connection -> Spec
|
spec :: SpecWith Application
|
||||||
spec struct c = beforeAll resetDb
|
spec =
|
||||||
. around (withApp cfgDefault struct c) $
|
|
||||||
describe "Deleting" $ do
|
describe "Deleting" $ do
|
||||||
context "existing record" $ do
|
context "existing record" $ do
|
||||||
it "succeeds with 204 and deletion count" $
|
it "succeeds with 204 and deletion count" $
|
||||||
|
|||||||
+36
-46
@@ -6,20 +6,20 @@ import Test.Hspec.Wai.JSON
|
|||||||
import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus))
|
import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus))
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
import PostgREST.Types (DbStructure(..))
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
|
import Data.Monoid ((<>))
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Control.Monad (replicateM_)
|
import Control.Monad (replicateM_, void)
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
|
||||||
import TestTypes(IncPK(..), CompoundPK(..))
|
import TestTypes(IncPK(..), CompoundPK(..))
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
spec :: DbStructure -> H.Connection -> Spec
|
spec :: SpecWith Application
|
||||||
spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
spec = do
|
||||||
describe "Posting new record" $ do
|
describe "Posting new record" $ do
|
||||||
context "disparate json types" $ do
|
context "disparate json types" $ do
|
||||||
it "accepts disparate json types" $ do
|
it "accepts disparate json types" $ do
|
||||||
@@ -42,7 +42,7 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
} |] `shouldRespondWith` ResponseMatcher {
|
} |] `shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|{"integer":14,"varchar":"testing!"}|]
|
matchBody = Just [str|{"integer":14,"varchar":"testing!"}|]
|
||||||
, matchStatus = 201
|
, matchStatus = 201
|
||||||
, matchHeaders = ["Content-Type" <:> "application/json"]
|
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "includes related data after insert" $
|
it "includes related data after insert" $
|
||||||
@@ -50,7 +50,7 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
[str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` ResponseMatcher {
|
[str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}|]
|
matchBody = Just [str|{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}|]
|
||||||
, matchStatus = 201
|
, matchStatus = 201
|
||||||
, matchHeaders = ["Content-Type" <:> "application/json", "Location" <:> "/projects?id=eq.6"]
|
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", "Location" <:> "/projects?id=eq.6"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -146,13 +146,6 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
, matchHeaders = ["Location" <:> [str|/json?data=eq.{"foo":"bar"}|]]
|
, matchHeaders = ["Location" <:> [str|/json?data=eq.{"foo":"bar"}|]]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- TODO! the test above seems right, why was the one below working before and not now
|
|
||||||
-- p <- request methodPost "/json" [("Prefer", "return=representation")] inserted
|
|
||||||
-- liftIO $ do
|
|
||||||
-- simpleBody p `shouldBe` inserted
|
|
||||||
-- simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%7B%22foo%22%3A%22bar%22%7D"
|
|
||||||
-- simpleStatus p `shouldBe` created201
|
|
||||||
|
|
||||||
it "serializes nested array" $ do
|
it "serializes nested array" $ do
|
||||||
let inserted = [json| { "data": [1,2,3] } |]
|
let inserted = [json| { "data": [1,2,3] } |]
|
||||||
request methodPost "/json"
|
request methodPost "/json"
|
||||||
@@ -163,12 +156,6 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
, matchStatus = 201
|
, matchStatus = 201
|
||||||
, matchHeaders = ["Location" <:> [str|/json?data=eq.[1,2,3]|]]
|
, matchHeaders = ["Location" <:> [str|/json?data=eq.[1,2,3]|]]
|
||||||
}
|
}
|
||||||
-- TODO! the test above seems right, why was the one below working before and not now
|
|
||||||
-- p <- request methodPost "/json" [("Prefer", "return=representation")] inserted
|
|
||||||
-- liftIO $ do
|
|
||||||
-- simpleBody p `shouldBe` inserted
|
|
||||||
-- simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%5B1%2C2%2C3%5D"
|
|
||||||
-- simpleStatus p `shouldBe` created201
|
|
||||||
|
|
||||||
describe "CSV insert" $ do
|
describe "CSV insert" $ do
|
||||||
|
|
||||||
@@ -184,16 +171,8 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just inserted
|
matchBody = Just inserted
|
||||||
, matchStatus = 201
|
, matchStatus = 201
|
||||||
, matchHeaders = ["Content-Type" <:> "text/csv"]
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
||||||
}
|
}
|
||||||
-- 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
|
|
||||||
|
|
||||||
context "requesting full representation" $ do
|
context "requesting full representation" $ do
|
||||||
it "returns full details of inserted record" $
|
it "returns full details of inserted record" $
|
||||||
@@ -203,21 +182,10 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "a,b\nbar,baz"
|
matchBody = Just "a,b\nbar,baz"
|
||||||
, matchStatus = 201
|
, matchStatus = 201
|
||||||
, matchHeaders = ["Content-Type" <:> "text/csv",
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
|
||||||
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
|
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
|
||||||
}
|
}
|
||||||
|
|
||||||
-- it "can post nulls (old way)" $ do
|
|
||||||
-- pendingWith "changed the response when in csv mode"
|
|
||||||
-- request methodPost "/no_pk"
|
|
||||||
-- [("Content-Type", "text/csv"), ("Prefer", "return=representation")]
|
|
||||||
-- "a,b\nNULL,foo"
|
|
||||||
-- `shouldRespondWith` ResponseMatcher {
|
|
||||||
-- matchBody = Just [json| { "a":null, "b":"foo" } |]
|
|
||||||
-- , matchStatus = 201
|
|
||||||
-- , matchHeaders = ["Content-Type" <:> "application/json",
|
|
||||||
-- "Location" <:> "/no_pk?a=is.null&b=eq.foo"]
|
|
||||||
-- }
|
|
||||||
it "can post nulls" $
|
it "can post nulls" $
|
||||||
request methodPost "/no_pk"
|
request methodPost "/no_pk"
|
||||||
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
|
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
|
||||||
@@ -225,7 +193,7 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "a,b\n,foo"
|
matchBody = Just "a,b\n,foo"
|
||||||
, matchStatus = 201
|
, matchStatus = 201
|
||||||
, matchHeaders = ["Content-Type" <:> "text/csv",
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
|
||||||
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
|
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,10 +202,21 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
it "fails for too few" $ do
|
it "fails for too few" $ do
|
||||||
p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"
|
p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"
|
||||||
liftIO $ simpleStatus p `shouldBe` badRequest400
|
liftIO $ simpleStatus p `shouldBe` badRequest400
|
||||||
-- it does not fail because the extra columns are ignored
|
|
||||||
-- it "fails for too many" $ do
|
context "with unicode values" $
|
||||||
-- p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz,bat,bad"
|
it "succeeds and returns usable location header" $ do
|
||||||
-- liftIO $ simpleStatus p `shouldBe` badRequest400
|
let payload = [json| { "a":"圍棋", "b":"¥" } |]
|
||||||
|
p <- request methodPost "/no_pk"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
payload
|
||||||
|
liftIO $ do
|
||||||
|
simpleBody p `shouldBe` payload
|
||||||
|
simpleStatus p `shouldBe` created201
|
||||||
|
|
||||||
|
let Just location = lookup hLocation $ simpleHeaders p
|
||||||
|
r <- get location
|
||||||
|
liftIO $ simpleBody r `shouldBe` "["<>payload<>"]"
|
||||||
|
|
||||||
|
|
||||||
describe "Putting record" $ do
|
describe "Putting record" $ do
|
||||||
|
|
||||||
@@ -388,6 +367,17 @@ spec struct c = beforeAll_ resetDb $ around (withApp cfgDefault struct c) $ do
|
|||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context "with unicode values" $
|
||||||
|
it "succeeds and returns values intact" $ do
|
||||||
|
void $ request methodPost "/no_pk" []
|
||||||
|
[json| { "a":"patchme", "b":"patchme" } |]
|
||||||
|
let payload = [json| { "a":"圍棋", "b":"¥" } |]
|
||||||
|
p <- request methodPatch "/no_pk?a=eq.patchme&b=eq.patchme"
|
||||||
|
[("Prefer", "return=representation")] payload
|
||||||
|
liftIO $ do
|
||||||
|
simpleBody p `shouldBe` "["<>payload<>"]"
|
||||||
|
simpleStatus p `shouldBe` ok200
|
||||||
|
|
||||||
describe "Row level permission" $
|
describe "Row level permission" $
|
||||||
it "set user_id when inserting rows" $ do
|
it "set user_id when inserting rows" $ do
|
||||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
||||||
|
|||||||
@@ -5,15 +5,12 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus))
|
import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus))
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
import PostgREST.Types (DbStructure(..))
|
import Network.Wai (Application)
|
||||||
|
|
||||||
spec :: DbStructure -> H.Connection -> Spec
|
spec :: SpecWith Application
|
||||||
spec struct c =
|
spec =
|
||||||
beforeAll resetDb
|
|
||||||
. around (withApp (cfgLimitRows 3) struct c) $
|
|
||||||
describe "Requesting many items with server limits enabled" $ do
|
describe "Requesting many items with server limits enabled" $ do
|
||||||
it "restricts results" $
|
it "restricts results" $
|
||||||
get "/items"
|
get "/items"
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Network.Wai.Test (SResponse(simpleHeaders))
|
import Network.Wai.Test (SResponse(simpleHeaders))
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
import PostgREST.Types (DbStructure(..))
|
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
spec :: DbStructure -> H.Connection -> Spec
|
spec :: SpecWith Application
|
||||||
spec struct c = around (withApp cfgDefault struct c) $ do
|
spec = do
|
||||||
|
|
||||||
describe "Querying a table with a column called count" $
|
describe "Querying a table with a column called count" $
|
||||||
it "should not confuse count column with pg_catalog.count aggregate" $
|
it "should not confuse count column with pg_catalog.count aggregate" $
|
||||||
@@ -247,6 +246,14 @@ spec struct c = around (withApp cfgDefault struct c) $ do
|
|||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "can combine multiple prefer values" $
|
||||||
|
request methodGet "/items?id=eq.5" [("Prefer","plurality=singular ; future=new; count=none")] ""
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| {"id":5} |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
it "works in the presence of a range header" $
|
it "works in the presence of a range header" $
|
||||||
let headers = ("Prefer","plurality=singular") :
|
let headers = ("Prefer","plurality=singular") :
|
||||||
rangeHdrs (ByteRangeFromTo 0 9) in
|
rangeHdrs (ByteRangeFromTo 0 9) in
|
||||||
@@ -338,7 +345,7 @@ spec struct c = around (withApp cfgDefault struct c) $ do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "k,extra\nxyyx,u\nxYYx,v"
|
matchBody = Just "k,extra\nxyyx,u\nxYYx,v"
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Type" <:> "text/csv"]
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "Canonical location" $ do
|
describe "Canonical location" $ do
|
||||||
@@ -371,7 +378,17 @@ spec struct c = around (withApp cfgDefault struct c) $ do
|
|||||||
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||||
|
|
||||||
describe "remote procedure call" $ do
|
describe "remote procedure call" $ do
|
||||||
context "a proc that returns a set" $
|
context "a proc that returns a set" $ do
|
||||||
|
it "returns paginated results" $
|
||||||
|
request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| [{"id":3}] |]
|
||||||
|
, matchStatus = 206
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
it "returns proper json" $
|
it "returns proper json" $
|
||||||
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
|
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
|
||||||
[json| [ {"id": 3}, {"id":4} ] |]
|
[json| [ {"id": 3}, {"id":4} ] |]
|
||||||
@@ -381,11 +398,15 @@ spec struct c = around (withApp cfgDefault struct c) $ do
|
|||||||
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
|
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
|
||||||
[json| [] |]
|
[json| [] |]
|
||||||
|
|
||||||
context "a proc that returns plain text" $
|
context "a proc that returns plain text" $ do
|
||||||
it "returns proper json" $
|
it "returns proper json" $
|
||||||
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
|
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
|
||||||
[json| [{"sayhello":"Hello, world"}] |]
|
[json| [{"sayhello":"Hello, world"}] |]
|
||||||
|
|
||||||
|
it "can handle unicode" $
|
||||||
|
post "/rpc/sayhello" [json| { "name": "¥" } |] `shouldRespondWith`
|
||||||
|
[json| [{"sayhello":"Hello, ¥"}] |]
|
||||||
|
|
||||||
context "improper input" $ do
|
context "improper input" $ do
|
||||||
it "rejects unknown content type even if payload is good" $
|
it "rejects unknown content type even if payload is good" $
|
||||||
request methodPost "/rpc/sayhello"
|
request methodPost "/rpc/sayhello"
|
||||||
@@ -413,6 +434,12 @@ spec struct c = around (withApp cfgDefault struct c) $ do
|
|||||||
it "GET with 405 on known procs" $
|
it "GET with 405 on known procs" $
|
||||||
get "/rpc/sayhello" `shouldRespondWith` 405
|
get "/rpc/sayhello" `shouldRespondWith` 405
|
||||||
|
|
||||||
|
it "executes the proc exactly once per request" $ do
|
||||||
|
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
|
||||||
|
[json| [{"callcounter":1}] |]
|
||||||
|
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
|
||||||
|
[json| [{"callcounter":2}] |]
|
||||||
|
|
||||||
describe "weird requests" $ do
|
describe "weird requests" $ do
|
||||||
it "can query as normal" $ do
|
it "can query as normal" $ do
|
||||||
get "/Escap3e;" `shouldRespondWith`
|
get "/Escap3e;" `shouldRespondWith`
|
||||||
|
|||||||
+100
-6
@@ -5,16 +5,110 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
|
import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
import PostgREST.Types (DbStructure(..))
|
import Network.Wai (Application)
|
||||||
|
|
||||||
spec :: DbStructure -> H.Connection -> Spec
|
defaultRange :: BL.ByteString
|
||||||
spec struct c = beforeAll resetDb
|
defaultRange = [json| { "min": 0, "max": 15 } |]
|
||||||
. around (withApp cfgDefault struct c) $
|
|
||||||
|
emptyRange :: BL.ByteString
|
||||||
|
emptyRange = [json| { "min": 2, "max": 2 } |]
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec = do
|
||||||
|
describe "POST /rpc/getitemrange" $ do
|
||||||
|
context "without range headers" $ do
|
||||||
|
context "with response under server size limit" $
|
||||||
|
it "returns whole range with status 200" $
|
||||||
|
post "/rpc/getitemrange" defaultRange `shouldRespondWith` 200
|
||||||
|
|
||||||
|
context "when I don't want the count" $ do
|
||||||
|
it "returns range Content-Range with */* for empty range" $
|
||||||
|
request methodPost "/rpc/getitemrange"
|
||||||
|
[("Prefer", "count=none")] emptyRange
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| [] |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns range Content-Range with range/*" $
|
||||||
|
request methodPost "/rpc/getitemrange"
|
||||||
|
[("Prefer", "count=none")] defaultRange
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-14/*"]
|
||||||
|
}
|
||||||
|
|
||||||
|
context "with range headers" $ do
|
||||||
|
|
||||||
|
context "of acceptable range" $ do
|
||||||
|
it "succeeds with partial content" $ do
|
||||||
|
r <- request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs $ ByteRangeFromTo 0 1) defaultRange
|
||||||
|
liftIO $ do
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Content-Range" "0-1/15"
|
||||||
|
simpleStatus r `shouldBe` partialContent206
|
||||||
|
|
||||||
|
it "understands open-ended ranges" $
|
||||||
|
request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs $ ByteRangeFrom 0) defaultRange
|
||||||
|
`shouldRespondWith` 200
|
||||||
|
|
||||||
|
it "returns an empty body when there are no results" $
|
||||||
|
request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs $ ByteRangeFromTo 0 1) emptyRange
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just "[]"
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "*/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "allows one-item requests" $ do
|
||||||
|
r <- request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs $ ByteRangeFromTo 0 0) defaultRange
|
||||||
|
liftIO $ do
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Content-Range" "0-0/15"
|
||||||
|
simpleStatus r `shouldBe` partialContent206
|
||||||
|
|
||||||
|
it "handles ranges beyond collection length via truncation" $ do
|
||||||
|
r <- request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs $ ByteRangeFromTo 10 100) defaultRange
|
||||||
|
liftIO $ do
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Content-Range" "10-14/15"
|
||||||
|
simpleStatus r `shouldBe` partialContent206
|
||||||
|
|
||||||
|
context "of invalid range" $ do
|
||||||
|
it "fails with 416 for offside range" $
|
||||||
|
request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs $ ByteRangeFromTo 1 0) emptyRange
|
||||||
|
`shouldRespondWith` 416
|
||||||
|
|
||||||
|
it "refuses a range with nonzero start when there are no items" $
|
||||||
|
request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs $ ByteRangeFromTo 1 2) emptyRange
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Nothing
|
||||||
|
, matchStatus = 416
|
||||||
|
, matchHeaders = ["Content-Range" <:> "*/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "refuses a range requesting start past last item" $
|
||||||
|
request methodPost "/rpc/getitemrange"
|
||||||
|
(rangeHdrs $ ByteRangeFromTo 100 199) defaultRange
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Nothing
|
||||||
|
, matchStatus = 416
|
||||||
|
, matchHeaders = ["Content-Range" <:> "*/15"]
|
||||||
|
}
|
||||||
describe "GET /items" $ do
|
describe "GET /items" $ do
|
||||||
|
|
||||||
context "without range headers" $ do
|
context "without range headers" $ do
|
||||||
context "with response under server size limit" $
|
context "with response under server size limit" $
|
||||||
it "returns whole range with status 200" $
|
it "returns whole range with status 200" $
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ module Feature.StructureSpec where
|
|||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
import PostgREST.Types (DbStructure(..))
|
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec = do
|
||||||
|
|
||||||
spec :: DbStructure -> H.Connection -> Spec
|
|
||||||
spec struct c = around (withApp cfgDefault struct c) $ do
|
|
||||||
describe "GET /" $ do
|
describe "GET /" $ do
|
||||||
it "lists views in schema" $
|
it "lists views in schema" $
|
||||||
request methodGet "/" [] ""
|
request methodGet "/" [] ""
|
||||||
@@ -24,6 +24,7 @@ spec struct c = around (withApp cfgDefault struct c) $ do
|
|||||||
, {"schema":"test","name":"comments","insertable":true}
|
, {"schema":"test","name":"comments","insertable":true}
|
||||||
, {"schema":"test","name":"complex_items","insertable":true}
|
, {"schema":"test","name":"complex_items","insertable":true}
|
||||||
, {"schema":"test","name":"compound_pk","insertable":true}
|
, {"schema":"test","name":"compound_pk","insertable":true}
|
||||||
|
, {"schema":"test","name":"filtered_tasks","insertable":true}
|
||||||
, {"schema":"test","name":"ghostBusters","insertable":true}
|
, {"schema":"test","name":"ghostBusters","insertable":true}
|
||||||
, {"schema":"test","name":"has_count_column","insertable":false}
|
, {"schema":"test","name":"has_count_column","insertable":false}
|
||||||
, {"schema":"test","name":"has_fk","insertable":true}
|
, {"schema":"test","name":"has_fk","insertable":true}
|
||||||
@@ -57,6 +58,61 @@ spec struct c = around (withApp cfgDefault struct c) $ do
|
|||||||
{matchStatus = 200}
|
{matchStatus = 200}
|
||||||
|
|
||||||
describe "Table info" $ do
|
describe "Table info" $ do
|
||||||
|
it "The structure of complex views is correctly detected" $
|
||||||
|
request methodOptions "/filtered_tasks" [] "" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
{
|
||||||
|
"pkey": [
|
||||||
|
"myId"
|
||||||
|
],
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"references": null,
|
||||||
|
"default": null,
|
||||||
|
"precision": 32,
|
||||||
|
"updatable": true,
|
||||||
|
"schema": "test",
|
||||||
|
"name": "myId",
|
||||||
|
"type": "integer",
|
||||||
|
"maxLen": null,
|
||||||
|
"enum": [],
|
||||||
|
"nullable": true,
|
||||||
|
"position": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"references": null,
|
||||||
|
"default": null,
|
||||||
|
"precision": null,
|
||||||
|
"updatable": true,
|
||||||
|
"schema": "test",
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"maxLen": null,
|
||||||
|
"enum": [],
|
||||||
|
"nullable": true,
|
||||||
|
"position": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"references": {
|
||||||
|
"schema": "test",
|
||||||
|
"column": "id",
|
||||||
|
"table": "projects"
|
||||||
|
},
|
||||||
|
"default": null,
|
||||||
|
"precision": 32,
|
||||||
|
"updatable": true,
|
||||||
|
"schema": "test",
|
||||||
|
"name": "projectID",
|
||||||
|
"type": "integer",
|
||||||
|
"maxLen": null,
|
||||||
|
"enum": [],
|
||||||
|
"nullable": true,
|
||||||
|
"position": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
it "is available with OPTIONS verb" $
|
it "is available with OPTIONS verb" $
|
||||||
request methodOptions "/menagerie" [] "" `shouldRespondWith`
|
request methodOptions "/menagerie" [] "" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module Feature.UnicodeSpec where
|
||||||
|
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Network.Wai (Application)
|
||||||
|
import Control.Monad (void)
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec =
|
||||||
|
describe "Reading and writing to unicode schema and table names" $
|
||||||
|
it "Can read and write values" $ do
|
||||||
|
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||||
|
`shouldRespondWith` "[]"
|
||||||
|
|
||||||
|
void $ post "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||||
|
[json| { "هویت": 1 } |]
|
||||||
|
|
||||||
|
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||||
|
`shouldRespondWith` [json| [{ "هویت": 1 }] |]
|
||||||
+32
-19
@@ -3,13 +3,14 @@ module Main where
|
|||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Pool as P
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
|
|
||||||
import PostgREST.DbStructure (getDbStructure)
|
import PostgREST.DbStructure (getDbStructure)
|
||||||
|
import PostgREST.App (postgrest)
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
|
|
||||||
import qualified Feature.AuthSpec
|
import qualified Feature.AuthSpec
|
||||||
|
import qualified Feature.ConcurrentSpec
|
||||||
import qualified Feature.CorsSpec
|
import qualified Feature.CorsSpec
|
||||||
import qualified Feature.DeleteSpec
|
import qualified Feature.DeleteSpec
|
||||||
import qualified Feature.InsertSpec
|
import qualified Feature.InsertSpec
|
||||||
@@ -17,27 +18,39 @@ import qualified Feature.QueryLimitedSpec
|
|||||||
import qualified Feature.QuerySpec
|
import qualified Feature.QuerySpec
|
||||||
import qualified Feature.RangeSpec
|
import qualified Feature.RangeSpec
|
||||||
import qualified Feature.StructureSpec
|
import qualified Feature.StructureSpec
|
||||||
|
import qualified Feature.UnicodeSpec
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
setupDb
|
setupDb
|
||||||
|
|
||||||
H.acquire (cs dbString) >>= \case
|
pool <- P.acquire (3, 10, cs testDbConn)
|
||||||
Left err -> error $ show err
|
|
||||||
Right c -> do
|
result <- P.use pool $ getDbStructure "test"
|
||||||
dbOrErr <- H.run (getDbStructure "test") c
|
let dbStructure = either (error.show) id result
|
||||||
-- Not using hspec-discover because we want to precompute
|
withApp = return $ postgrest testCfg dbStructure pool
|
||||||
-- the db structure and pass it to specs for speed
|
ltdApp = return $ postgrest testLtdRowsCfg dbStructure pool
|
||||||
either (error.show) (hspec . specs c) dbOrErr
|
unicodeApp = return $ postgrest testUnicodeCfg dbStructure pool
|
||||||
H.release c
|
|
||||||
|
hspec $ do
|
||||||
|
mapM_ (beforeAll_ resetDb . before withApp) specs
|
||||||
|
|
||||||
|
-- this test runs with a different server flag
|
||||||
|
beforeAll_ resetDb . before ltdApp $
|
||||||
|
describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec
|
||||||
|
|
||||||
|
-- this test runs with a different schema
|
||||||
|
beforeAll_ resetDb . before unicodeApp $
|
||||||
|
describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec
|
||||||
|
|
||||||
where
|
where
|
||||||
specs conn dbStructure = do
|
specs = map (uncurry describe) [
|
||||||
describe "Feature.AuthSpec" $ Feature.AuthSpec.spec dbStructure conn
|
("Feature.AuthSpec" , Feature.AuthSpec.spec)
|
||||||
describe "Feature.CorsSpec" $ Feature.CorsSpec.spec dbStructure conn
|
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
|
||||||
describe "Feature.DeleteSpec" $ Feature.DeleteSpec.spec dbStructure conn
|
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
||||||
describe "Feature.InsertSpec" $ Feature.InsertSpec.spec dbStructure conn
|
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
|
||||||
describe "Feature.QueryLimitedSpec" $ Feature.QueryLimitedSpec.spec dbStructure conn
|
, ("Feature.InsertSpec" , Feature.InsertSpec.spec)
|
||||||
describe "Feature.QuerySpec" $ Feature.QuerySpec.spec dbStructure conn
|
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
|
||||||
describe "Feature.RangeSpec" $ Feature.RangeSpec.spec dbStructure conn
|
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
|
||||||
describe "Feature.StructureSpec" $ Feature.StructureSpec.spec dbStructure conn
|
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
|
||||||
|
]
|
||||||
|
|||||||
+11
-35
@@ -1,10 +1,6 @@
|
|||||||
module SpecHelper where
|
module SpecHelper where
|
||||||
|
|
||||||
import Network.Wai
|
|
||||||
import Test.Hspec
|
|
||||||
|
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
|
||||||
import Control.Monad (void)
|
import Control.Monad (void)
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
||||||
@@ -16,42 +12,22 @@ import qualified Data.ByteString.Char8 as BS
|
|||||||
import System.Process (readProcess)
|
import System.Process (readProcess)
|
||||||
import Web.JWT (secret)
|
import Web.JWT (secret)
|
||||||
|
|
||||||
import qualified Hasql.Connection as H
|
|
||||||
import qualified Hasql.Session as H
|
|
||||||
|
|
||||||
import PostgREST.App (app)
|
|
||||||
import PostgREST.Config (AppConfig(..))
|
import PostgREST.Config (AppConfig(..))
|
||||||
import PostgREST.Middleware
|
|
||||||
import PostgREST.Error(pgErrResponse)
|
|
||||||
import PostgREST.Types
|
|
||||||
import PostgREST.QueryBuilder (inTransaction, Isolation(..))
|
|
||||||
|
|
||||||
dbString :: String
|
testDbConn :: String
|
||||||
dbString = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_test"
|
testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_test"
|
||||||
|
|
||||||
cfg :: String -> Maybe Integer -> AppConfig
|
testCfg :: AppConfig
|
||||||
cfg conStr = AppConfig conStr "postgrest_test_anonymous" "test" 3000 (secret "safe") 10
|
testCfg =
|
||||||
|
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 Nothing True
|
||||||
|
|
||||||
cfgDefault :: AppConfig
|
testUnicodeCfg :: AppConfig
|
||||||
cfgDefault = cfg dbString Nothing
|
testUnicodeCfg =
|
||||||
|
AppConfig testDbConn "postgrest_test_anonymous" "تست" 3000 (secret "safe") 10 Nothing True
|
||||||
|
|
||||||
cfgLimitRows :: Integer -> AppConfig
|
testLtdRowsCfg :: AppConfig
|
||||||
cfgLimitRows = cfg dbString . Just
|
testLtdRowsCfg =
|
||||||
|
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 (Just 3) True
|
||||||
withApp :: AppConfig -> DbStructure -> H.Connection
|
|
||||||
-> ActionWith Application -> IO ()
|
|
||||||
withApp config dbStructure c perform =
|
|
||||||
perform $ defaultMiddle $ \req resp -> do
|
|
||||||
time <- getPOSIXTime
|
|
||||||
body <- strictRequestBody req
|
|
||||||
let handleReq = H.run $ inTransaction ReadCommitted
|
|
||||||
(runWithClaims config time (app dbStructure config body) req)
|
|
||||||
|
|
||||||
handleReq c >>= \case
|
|
||||||
Left err -> do
|
|
||||||
void $ H.run (H.sql "rollback;") c
|
|
||||||
resp $ pgErrResponse err
|
|
||||||
Right res -> resp res
|
|
||||||
|
|
||||||
setupDb :: IO ()
|
setupDb :: IO ()
|
||||||
setupDb = do
|
setupDb = do
|
||||||
|
|||||||
Vendored
+5
-1
@@ -2,10 +2,11 @@
|
|||||||
GRANT USAGE ON SCHEMA
|
GRANT USAGE ON SCHEMA
|
||||||
postgrest
|
postgrest
|
||||||
, test
|
, test
|
||||||
|
, "تست"
|
||||||
TO postgrest_test_anonymous;
|
TO postgrest_test_anonymous;
|
||||||
|
|
||||||
-- Schema test objects
|
-- Schema test objects
|
||||||
SET search_path = test, pg_catalog;
|
SET search_path = test, "تست", pg_catalog;
|
||||||
|
|
||||||
GRANT ALL ON TABLE
|
GRANT ALL ON TABLE
|
||||||
items
|
items
|
||||||
@@ -28,6 +29,7 @@ GRANT ALL ON TABLE
|
|||||||
, projects_view
|
, projects_view
|
||||||
, simple_pk
|
, simple_pk
|
||||||
, tasks
|
, tasks
|
||||||
|
, filtered_tasks
|
||||||
, tsearch
|
, tsearch
|
||||||
, users
|
, users
|
||||||
, users_projects
|
, users_projects
|
||||||
@@ -35,6 +37,7 @@ GRANT ALL ON TABLE
|
|||||||
, "Escap3e;"
|
, "Escap3e;"
|
||||||
, "ghostBusters"
|
, "ghostBusters"
|
||||||
, "withUnique"
|
, "withUnique"
|
||||||
|
, "موارد"
|
||||||
TO postgrest_test_anonymous;
|
TO postgrest_test_anonymous;
|
||||||
|
|
||||||
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
||||||
@@ -42,6 +45,7 @@ GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
|||||||
GRANT USAGE ON SEQUENCE
|
GRANT USAGE ON SEQUENCE
|
||||||
auto_incrementing_pk_id_seq
|
auto_incrementing_pk_id_seq
|
||||||
, items_id_seq
|
, items_id_seq
|
||||||
|
, callcounter_count
|
||||||
TO postgrest_test_anonymous;
|
TO postgrest_test_anonymous;
|
||||||
|
|
||||||
-- Privileges for non anonymous users
|
-- Privileges for non anonymous users
|
||||||
|
|||||||
Vendored
+100
-10
@@ -33,6 +33,13 @@ CREATE SCHEMA private;
|
|||||||
CREATE SCHEMA test;
|
CREATE SCHEMA test;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: تست; Type: SCHEMA; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SCHEMA تست;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
|
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
|
||||||
--
|
--
|
||||||
@@ -50,6 +57,23 @@ CREATE TYPE jwt_claims AS (
|
|||||||
id text
|
id text
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: big_jwt_claims; Type: TYPE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TYPE big_jwt_claims AS (
|
||||||
|
iss text,
|
||||||
|
sub text,
|
||||||
|
aud text,
|
||||||
|
exp integer,
|
||||||
|
nbf integer,
|
||||||
|
iat integer,
|
||||||
|
jti text,
|
||||||
|
|
||||||
|
role text,
|
||||||
|
"http://postgrest.com/foo" boolean
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
SET search_path = test, pg_catalog;
|
SET search_path = test, pg_catalog;
|
||||||
|
|
||||||
@@ -145,6 +169,14 @@ CREATE FUNCTION anti_id(test.items) RETURNS bigint
|
|||||||
AS $_$ SELECT $1.id * -1 $_$;
|
AS $_$ SELECT $1.id * -1 $_$;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
SET search_path = تست, pg_catalog;
|
||||||
|
|
||||||
|
CREATE TABLE موارد (
|
||||||
|
هویت bigint NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
SET search_path = test, pg_catalog;
|
SET search_path = test, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -183,6 +215,43 @@ SELECT rolname::text, id::text FROM postgrest.auth WHERE id = id AND pass = pass
|
|||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION jwt_test() RETURNS public.big_jwt_claims
|
||||||
|
LANGUAGE sql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
|
||||||
|
1300819380 as exp, 1300819380 as nbf, 1300819380 as iat,
|
||||||
|
'foo'::text as jti, 'postgrest_test'::text as role,
|
||||||
|
true as "http://postgrest.com/foo";
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: reveal_big_jwt(); Type: FUNCTION; Schema: test; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE FUNCTION reveal_big_jwt() RETURNS TABLE (
|
||||||
|
iss text, sub text, aud text, exp bigint,
|
||||||
|
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
|
||||||
|
)
|
||||||
|
LANGUAGE sql SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
SELECT current_setting('postgrest.claims.iss') as iss,
|
||||||
|
current_setting('postgrest.claims.sub') as sub,
|
||||||
|
current_setting('postgrest.claims.aud') as aud,
|
||||||
|
current_setting('postgrest.claims.exp')::bigint as exp,
|
||||||
|
current_setting('postgrest.claims.nbf')::bigint as nbf,
|
||||||
|
current_setting('postgrest.claims.iat')::bigint as iat,
|
||||||
|
current_setting('postgrest.claims.jti') as jti,
|
||||||
|
-- role is not included in the claims list
|
||||||
|
current_setting('postgrest.claims.http://postgrest.com/foo')::boolean
|
||||||
|
as "http://postgrest.com/foo";
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: problem(); Type: FUNCTION; Schema: test; Owner: -
|
-- Name: problem(); Type: FUNCTION; Schema: test; Owner: -
|
||||||
--
|
--
|
||||||
@@ -207,6 +276,18 @@ CREATE FUNCTION sayhello(name text) RETURNS text
|
|||||||
$_$;
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: callcounter(); Type: FUNCTION; Schema: test; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE callcounter_count START 1;
|
||||||
|
|
||||||
|
CREATE FUNCTION callcounter() RETURNS bigint
|
||||||
|
LANGUAGE sql
|
||||||
|
AS $_$
|
||||||
|
SELECT nextval('test.callcounter_count');
|
||||||
|
$_$;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: test_empty_rowset(); Type: FUNCTION; Schema: test; Owner: -
|
-- Name: test_empty_rowset(); Type: FUNCTION; Schema: test; Owner: -
|
||||||
--
|
--
|
||||||
@@ -540,6 +621,15 @@ CREATE TABLE simple_pk (
|
|||||||
extra character varying NOT NULL
|
extra character varying NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users_projects; Type: TABLE; Schema: test; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE users_projects (
|
||||||
|
user_id integer NOT NULL,
|
||||||
|
project_id integer NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: tasks; Type: TABLE; Schema: test; Owner: -
|
-- Name: tasks; Type: TABLE; Schema: test; Owner: -
|
||||||
@@ -551,6 +641,16 @@ CREATE TABLE tasks (
|
|||||||
project_id integer
|
project_id integer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW filtered_tasks AS
|
||||||
|
SELECT id AS "myId", name, project_id AS "projectID"
|
||||||
|
FROM tasks
|
||||||
|
WHERE project_id IN (
|
||||||
|
SELECT id FROM projects WHERE id = 1
|
||||||
|
) AND
|
||||||
|
project_id IN (
|
||||||
|
SELECT project_id FROM users_projects WHERE user_id = 1
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: tsearch; Type: TABLE; Schema: test; Owner: -
|
-- Name: tsearch; Type: TABLE; Schema: test; Owner: -
|
||||||
@@ -571,15 +671,6 @@ CREATE TABLE users (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: users_projects; Type: TABLE; Schema: test; Owner: -
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE users_projects (
|
|
||||||
user_id integer NOT NULL,
|
|
||||||
project_id integer NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: users_tasks; Type: TABLE; Schema: test; Owner: -
|
-- Name: users_tasks; Type: TABLE; Schema: test; Owner: -
|
||||||
@@ -909,7 +1000,6 @@ ALTER TABLE ONLY users_tasks
|
|||||||
ALTER TABLE ONLY users_tasks
|
ALTER TABLE ONLY users_tasks
|
||||||
ADD CONSTRAINT users_tasks_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
ADD CONSTRAINT users_tasks_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|||||||
Reference in New Issue
Block a user