Merge pull request #276 from ruslantalpa/master
Support for &select=col1,col2,col3 as suggested in issue #227
This commit is contained in:
@@ -64,7 +64,7 @@ app conf reqBody req =
|
|||||||
else do
|
else do
|
||||||
let qt = qualify table
|
let qt = qualify table
|
||||||
from = fromMaybe 0 $ rangeOffset <$> range
|
from = fromMaybe 0 $ rangeOffset <$> range
|
||||||
select = B.Stmt "select " V.empty True <>
|
query = B.Stmt "select " V.empty True <>
|
||||||
parentheticT (
|
parentheticT (
|
||||||
whereT qt qq $ countRows qt
|
whereT qt qq $ countRows qt
|
||||||
) <> commaq <> (
|
) <> commaq <> (
|
||||||
@@ -72,9 +72,9 @@ app conf reqBody req =
|
|||||||
. limitT range
|
. limitT range
|
||||||
. orderT (orderParse qq)
|
. orderT (orderParse qq)
|
||||||
. whereT qt qq
|
. whereT qt qq
|
||||||
$ selectStar qt
|
$ select qt qq
|
||||||
)
|
)
|
||||||
row <- H.maybeEx select
|
row <- H.maybeEx query
|
||||||
let (tableTotal, queryTotal, body) =
|
let (tableTotal, queryTotal, body) =
|
||||||
fromMaybe (0, 0, Just "" :: Maybe Text) row
|
fromMaybe (0, 0, Just "" :: Maybe Text) row
|
||||||
to = from+queryTotal-1
|
to = from+queryTotal-1
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ whereT table params q =
|
|||||||
then q
|
then q
|
||||||
else q <> B.Stmt " where " empty True <> conjunction
|
else q <> B.Stmt " where " empty True <> conjunction
|
||||||
where
|
where
|
||||||
cols = [ col | col <- params, fst col `notElem` ["order"] ]
|
cols = [ col | col <- params, fst col `notElem` ["order","select"] ]
|
||||||
wherePredTable = wherePred table
|
wherePredTable = wherePred table
|
||||||
conjunction = mconcat $ L.intersperse andq (map wherePredTable cols)
|
conjunction = mconcat $ L.intersperse andq (map wherePredTable cols)
|
||||||
|
|
||||||
@@ -129,6 +129,29 @@ asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <
|
|||||||
selectStar :: QualifiedIdentifier -> PStmt
|
selectStar :: QualifiedIdentifier -> PStmt
|
||||||
selectStar t = B.Stmt ("select * from " <> fromQi t) empty True
|
selectStar t = B.Stmt ("select * from " <> fromQi t) empty True
|
||||||
|
|
||||||
|
select :: QualifiedIdentifier -> Net.Query -> PStmt
|
||||||
|
select table params =
|
||||||
|
if L.null cols
|
||||||
|
then selectStar table
|
||||||
|
else B.Stmt "select " empty True <> conjunction <> B.Stmt (" from " <> fromQi table ) empty True
|
||||||
|
where
|
||||||
|
selectTermTable = selectTerm table
|
||||||
|
conjunction = mconcat $ L.intersperse commaq (map selectTermTable cols)
|
||||||
|
columnsParam = fromMaybe "" $ join (lookup "select" params)
|
||||||
|
cols = filter ((>0) . T.length) $ map T.strip $ T.split (==',') $ cs columnsParam
|
||||||
|
|
||||||
|
selectTerm :: QualifiedIdentifier -> T.Text -> PStmt
|
||||||
|
selectTerm table col =
|
||||||
|
case T.splitOn "::" col of
|
||||||
|
[colName,castTo] -> B.Stmt ("CAST (" <> pgFmtJsonbPath table (cs colName) <> " AS " <> castToSafe <> " )" <> asT (jsonbPath colName)) empty True
|
||||||
|
where castToSafe = T.filter ( `elem` ['a'..'z'] ) castTo
|
||||||
|
_-> B.Stmt (pgFmtJsonbPath table (cs col) <> asT (jsonbPath col)) empty True
|
||||||
|
where
|
||||||
|
jsonbPath :: T.Text -> Maybe JsonbPath
|
||||||
|
jsonbPath c = parseJsonbPath $ cs c
|
||||||
|
asT (Just (DoubleArrow _ (KeyIdentifier key))) = " AS " <> pgFmtIdent key
|
||||||
|
asT _ = ""
|
||||||
|
|
||||||
returningStarT :: StatementT
|
returningStarT :: StatementT
|
||||||
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
|
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import SpecHelper
|
|||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec =
|
spec =
|
||||||
beforeAll (clearTable "items" >> createItems 15)
|
beforeAll (clearTable "items" >> createItems 15)
|
||||||
|
. beforeAll (clearTable "complex_items" >> createComplexItems)
|
||||||
. beforeAll (clearTable "nullable_integer" >> createNullInteger)
|
. beforeAll (clearTable "nullable_integer" >> createNullInteger)
|
||||||
. beforeAll (
|
. beforeAll (
|
||||||
clearTable "no_pk" >>
|
clearTable "no_pk" >>
|
||||||
createNulls 2 >>
|
createNulls 2 >>
|
||||||
createLikableStrings >>
|
createLikableStrings >>
|
||||||
createJsonData)
|
createJsonData)
|
||||||
. afterAll_ (clearTable "items" >> clearTable "no_pk" >> clearTable "simple_pk")
|
. afterAll_ (clearTable "items" >> clearTable "complex_items" >> clearTable "no_pk" >> clearTable "simple_pk")
|
||||||
. around withApp $ do
|
. around withApp $ do
|
||||||
|
|
||||||
describe "Querying a table with a column called count" $
|
describe "Querying a table with a column called count" $
|
||||||
@@ -129,6 +130,55 @@ spec =
|
|||||||
get "/items?always_true=eq.true" `shouldRespondWith`
|
get "/items?always_true=eq.true" `shouldRespondWith`
|
||||||
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
||||||
|
|
||||||
|
describe "Shaping response with select parameter" $ do
|
||||||
|
|
||||||
|
it "selectStar works in absense of parameter" $
|
||||||
|
get "/complex_items?id=eq.3" `shouldRespondWith`
|
||||||
|
"[{\"id\":3,\"name\":\"Three\",\"settings\":{\"foo\":{\"int\":1,\"bar\":\"baz\"}}}]"
|
||||||
|
|
||||||
|
it "one simple column" $
|
||||||
|
get "/complex_items?select=id" `shouldRespondWith`
|
||||||
|
[json| [{"id":1},{"id":2},{"id":3}] |]
|
||||||
|
|
||||||
|
it "one simple column with casting (text)" $
|
||||||
|
get "/complex_items?select=id::text" `shouldRespondWith`
|
||||||
|
[json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]
|
||||||
|
|
||||||
|
it "json column" $
|
||||||
|
get "/complex_items?id=eq.1&select=settings" `shouldRespondWith`
|
||||||
|
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
|
||||||
|
|
||||||
|
it "json subfield one level with casting (json)" $
|
||||||
|
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
|
||||||
|
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
|
||||||
|
|
||||||
|
it "fails on bad casting (data of the wrong format)" $
|
||||||
|
get "/complex_items?select=settings->foo->>bar::integer"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
|
||||||
|
, matchStatus = 400
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
|
it "fails on bad casting (wrong cast type)" $
|
||||||
|
get "/complex_items?select=id::fakecolumntype"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
|
||||||
|
, matchStatus = 400
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
it "json subfield two levels (string)" $
|
||||||
|
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
|
||||||
|
[json| [{"bar":"baz"}] |]
|
||||||
|
|
||||||
|
|
||||||
|
it "json subfield two levels with casting (int)" $
|
||||||
|
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
|
||||||
|
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
|
||||||
|
|
||||||
|
|
||||||
describe "ordering response" $ do
|
describe "ordering response" $ do
|
||||||
it "by a column asc" $
|
it "by a column asc" $
|
||||||
get "/items?id=lte.2&order=id.asc"
|
get "/items?id=lte.2&order=id.asc"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ spec = around withApp $ do
|
|||||||
request methodGet "/" [] ""
|
request methodGet "/" [] ""
|
||||||
`shouldRespondWith` [json| [
|
`shouldRespondWith` [json| [
|
||||||
{"schema":"1","name":"auto_incrementing_pk","insertable":true}
|
{"schema":"1","name":"auto_incrementing_pk","insertable":true}
|
||||||
|
, {"schema":"1","name":"complex_items","insertable":true}
|
||||||
, {"schema":"1","name":"compound_pk","insertable":true}
|
, {"schema":"1","name":"compound_pk","insertable":true}
|
||||||
, {"schema":"1","name":"has_count_column","insertable":false}
|
, {"schema":"1","name":"has_count_column","insertable":false}
|
||||||
, {"schema":"1","name":"has_fk","insertable":true}
|
, {"schema":"1","name":"has_fk","insertable":true}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Data.Monoid
|
|||||||
import Data.Text hiding (map)
|
import Data.Text hiding (map)
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import Control.Monad (void)
|
import Control.Monad (void)
|
||||||
|
import Control.Applicative
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
||||||
hRange, hAuthorization, hAccept)
|
hRange, hAuthorization, hAccept)
|
||||||
@@ -118,6 +119,18 @@ createItems n = do
|
|||||||
txn = mapM_ H.unitEx stmts
|
txn = mapM_ H.unitEx stmts
|
||||||
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
|
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
|
||||||
|
|
||||||
|
createComplexItems :: IO ()
|
||||||
|
createComplexItems = do
|
||||||
|
pool <- testPool
|
||||||
|
void . liftIO $ H.session pool $ H.tx Nothing txn
|
||||||
|
where
|
||||||
|
txn = mapM_ H.unitEx stmts
|
||||||
|
stmts = getZipList $ [H.stmt|insert into "1".complex_items (id, name, settings) values (?,?,?)|]
|
||||||
|
<$> ZipList ([1..3]::[Int])
|
||||||
|
<*> ZipList (["One", "Two", "Three"]::[Text])
|
||||||
|
<*> ZipList ([jobj,jobj,jobj])
|
||||||
|
jobj = (J.object [("foo", J.object [("int", J.Number 1),("bar", J.String "baz")])])
|
||||||
|
|
||||||
createNulls :: Int -> IO ()
|
createNulls :: Int -> IO ()
|
||||||
createNulls n = do
|
createNulls n = do
|
||||||
pool <- testPool
|
pool <- testPool
|
||||||
|
|||||||
Vendored
+22
-6
@@ -47,7 +47,7 @@ SET search_path = postgrest, pg_catalog;
|
|||||||
CREATE FUNCTION check_role_exists() RETURNS trigger
|
CREATE FUNCTION check_role_exists() RETURNS trigger
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
begin
|
begin
|
||||||
if not exists (select 1 from pg_roles as r where r.rolname = new.rolname) then
|
if not exists (select 1 from pg_roles as r where r.rolname = new.rolname) then
|
||||||
raise foreign_key_violation using message = 'Cannot create user with unknown role: ' || new.rolname;
|
raise foreign_key_violation using message = 'Cannot create user with unknown role: ' || new.rolname;
|
||||||
return null;
|
return null;
|
||||||
@@ -64,7 +64,7 @@ CREATE FUNCTION update_owner() RETURNS trigger
|
|||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
NEW.owner = current_user;
|
NEW.owner = current_user;
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
@@ -75,8 +75,8 @@ ALTER FUNCTION postgrest.update_owner() OWNER TO postgrest_test;
|
|||||||
CREATE FUNCTION set_authors_only_owner() RETURNS trigger
|
CREATE FUNCTION set_authors_only_owner() RETURNS trigger
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
begin
|
begin
|
||||||
NEW.owner = current_setting('user_vars.user_id');
|
NEW.owner = current_setting('user_vars.user_id');
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
end
|
end
|
||||||
$$;
|
$$;
|
||||||
@@ -170,7 +170,7 @@ ALTER TABLE "1".has_fk_id_seq OWNER TO postgrest_test;
|
|||||||
ALTER SEQUENCE has_fk_id_seq OWNED BY has_fk.id;
|
ALTER SEQUENCE has_fk_id_seq OWNED BY has_fk.id;
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW "1".materialized_view AS
|
CREATE MATERIALIZED VIEW "1".materialized_view AS
|
||||||
SELECT
|
SELECT
|
||||||
version();
|
version();
|
||||||
|
|
||||||
ALTER TABLE "1".materialized_view OWNER TO postgrest_test;
|
ALTER TABLE "1".materialized_view OWNER TO postgrest_test;
|
||||||
@@ -201,6 +201,15 @@ CREATE TABLE items (
|
|||||||
|
|
||||||
ALTER TABLE "1".items OWNER TO postgrest_test;
|
ALTER TABLE "1".items OWNER TO postgrest_test;
|
||||||
|
|
||||||
|
CREATE TABLE complex_items (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
name text,
|
||||||
|
settings json
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE "1".complex_items OWNER TO postgrest_test;
|
||||||
|
|
||||||
|
|
||||||
CREATE SEQUENCE items_id_seq
|
CREATE SEQUENCE items_id_seq
|
||||||
START WITH 1
|
START WITH 1
|
||||||
@@ -407,7 +416,7 @@ ALTER FUNCTION public.always_true("1".items) OWNER TO postgrest_test;
|
|||||||
|
|
||||||
ALTER TABLE ONLY authors_only
|
ALTER TABLE ONLY authors_only
|
||||||
ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret);
|
ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret);
|
||||||
|
|
||||||
CREATE TRIGGER insert_insertable_view_with_join INSTEAD OF INSERT ON "1".insertable_view_with_join FOR EACH ROW EXECUTE PROCEDURE "1".insert_insertable_view_with_join();
|
CREATE TRIGGER insert_insertable_view_with_join INSTEAD OF INSERT ON "1".insertable_view_with_join FOR EACH ROW EXECUTE PROCEDURE "1".insert_insertable_view_with_join();
|
||||||
|
|
||||||
|
|
||||||
@@ -437,6 +446,8 @@ ALTER TABLE ONLY has_fk
|
|||||||
ALTER TABLE ONLY items
|
ALTER TABLE ONLY items
|
||||||
ADD CONSTRAINT items_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT items_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY complex_items
|
||||||
|
ADD CONSTRAINT complex_items_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE ONLY menagerie
|
ALTER TABLE ONLY menagerie
|
||||||
@@ -539,6 +550,11 @@ REVOKE ALL ON TABLE items FROM postgrest_test;
|
|||||||
GRANT ALL ON TABLE items TO postgrest_test;
|
GRANT ALL ON TABLE items TO postgrest_test;
|
||||||
GRANT ALL ON TABLE items TO postgrest_anonymous;
|
GRANT ALL ON TABLE items TO postgrest_anonymous;
|
||||||
|
|
||||||
|
REVOKE ALL ON TABLE complex_items FROM PUBLIC;
|
||||||
|
REVOKE ALL ON TABLE complex_items FROM postgrest_test;
|
||||||
|
GRANT ALL ON TABLE complex_items TO postgrest_test;
|
||||||
|
GRANT ALL ON TABLE complex_items TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
REVOKE ALL ON FUNCTION getitemrange(bigint, bigint) FROM PUBLIC;
|
REVOKE ALL ON FUNCTION getitemrange(bigint, bigint) FROM PUBLIC;
|
||||||
REVOKE ALL ON FUNCTION getitemrange(bigint, bigint) FROM postgrest_test;
|
REVOKE ALL ON FUNCTION getitemrange(bigint, bigint) FROM postgrest_test;
|
||||||
|
|||||||
Reference in New Issue
Block a user