diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 04f03456e..50e524618 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -51,9 +51,9 @@ app conf reqBody req = return $ responseLBS status200 [jsonH] $ cs body ([table], "OPTIONS") -> do - let t = QualifiedTable schema (cs table) - cols <- columns t - pkey <- map cs <$> primaryKeyColumns t + let qt = qualify table + cols <- columns qt + pkey <- map cs <$> primaryKeyColumns qt return $ responseLBS status200 [jsonH, allOrigins] $ encode (TableOptions cols pkey) @@ -61,15 +61,15 @@ app conf reqBody req = if range == Just emptyRange then return $ responseLBS status416 [] "HTTP Range error" else do - let qt = QualifiedTable schema (cs table) - let select = B.Stmt "select " V.empty True <> + let qt = qualify table + select = B.Stmt "select " V.empty True <> parentheticT ( - whereT qq $ countRows qt + whereT qt qq $ countRows qt ) <> commaq <> ( asJsonWithCount . limitT range . orderT (orderParse qq) - . whereT qq + . whereT qt qq $ selectStar qt ) row <- H.maybeEx select @@ -128,7 +128,7 @@ app conf reqBody req = encode . object $ [("message", String "Failed authentication.")] ([table], "POST") -> do - let qt = QualifiedTable schema (cs table) + let qt = qualify table echoRequested = lookup "Prefer" hdrs == Just "return=representation" parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value)) parsed = if lookup "Content-Type" hdrs == Just "text/csv" @@ -164,7 +164,7 @@ app conf reqBody req = ([table], "PUT") -> handleJsonObj reqBody $ \obj -> do - let qt = QualifiedTable schema (cs table) + let qt = qualify table primaryKeys <- primaryKeyColumns qt let specifiedKeys = map (cs . fst) qq if S.fromList primaryKeys /= S.fromList specifiedKeys @@ -177,7 +177,7 @@ app conf reqBody req = then do let vals = M.elems obj H.unitEx $ iffNotT - (whereT qq $ update qt cols vals) + (whereT qt qq $ update qt cols vals) (insertSelect qt cols vals) return $ responseLBS status204 [ jsonH ] "" @@ -188,9 +188,9 @@ app conf reqBody req = ([table], "PATCH") -> handleJsonObj reqBody $ \obj -> do - let qt = QualifiedTable schema (cs table) + let qt = qualify table up = returningStarT - . whereT qq + . whereT qt qq $ update qt (map cs $ M.keys obj) (M.elems obj) patch = withT up "t" $ B.Stmt "select count(t), array_to_json(array_agg(row_to_json(t)))::character varying" @@ -207,10 +207,10 @@ app conf reqBody req = return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else "" ([table], "DELETE") -> do - let qt = QualifiedTable schema (cs table) + let qt = qualify table let del = countT . returningStarT - . whereT qq + . whereT qt qq $ deleteFrom qt row <- H.maybeEx del let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row @@ -222,15 +222,16 @@ app conf reqBody req = return $ responseLBS status404 [] "" where - path = pathInfo req - verb = requestMethod req - qq = queryString req - hdrs = requestHeaders req - schema = requestedSchema (cs $ configV1Schema conf) hdrs + path = pathInfo req + verb = requestMethod req + qq = queryString req + qualify = QualifiedTable schema + hdrs = requestHeaders req + schema = requestedSchema (cs $ configV1Schema conf) hdrs authenticator = cs $ configDbUser conf - jwtSecret = cs $ configJwtSecret conf - range = rangeRequested hdrs - allOrigins = ("Access-Control-Allow-Origin", "*") :: Header + jwtSecret = cs $ configJwtSecret conf + range = rangeRequested hdrs + allOrigins = ("Access-Control-Allow-Origin", "*") :: Header sqlError :: t sqlError = undefined diff --git a/src/PostgREST/PgQuery.hs b/src/PostgREST/PgQuery.hs index 805826ca6..6d678f394 100644 --- a/src/PostgREST/PgQuery.hs +++ b/src/PostgREST/PgQuery.hs @@ -51,14 +51,15 @@ limitT r q = limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r -whereT :: Net.Query -> StatementT -whereT params q = +whereT :: QualifiedTable -> Net.Query -> StatementT +whereT table params q = if L.null cols then q else q <> B.Stmt " where " empty True <> conjunction where cols = [ col | col <- params, fst col `notElem` ["order"] ] - conjunction = mconcat $ L.intersperse andq (map wherePred cols) + wherePredTable = wherePred table + conjunction = mconcat $ L.intersperse andq (map wherePredTable cols) withT :: PStmt -> T.Text -> StatementT withT (B.Stmt eq ep epre) v (B.Stmt wq wp wpre) = @@ -154,9 +155,9 @@ update t cols vals = B.Stmt <> ")") empty True -wherePred :: Net.QueryItem -> PStmt -wherePred (col, predicate) = - B.Stmt (" " <> pgFmtJsonbPath (cs col) <> " " <> op <> " " <> +wherePred :: QualifiedTable -> Net.QueryItem -> PStmt +wherePred table (col, predicate) = + B.Stmt (" " <> pgFmtJsonbPath table (cs col) <> " " <> op <> " " <> if opCode `elem` ["is","isnot"] then whiteList value else cs sqlValue) empty True @@ -237,11 +238,11 @@ parseJsonbPath p = (KeyIdentifier b) _ -> Nothing -pgFmtJsonbPath :: T.Text -> T.Text -pgFmtJsonbPath p = +pgFmtJsonbPath :: QualifiedTable -> T.Text -> T.Text +pgFmtJsonbPath table p = pgFmtJsonbPath' $ fromMaybe (ColIdentifier p) (parseJsonbPath p) where - pgFmtJsonbPath' (ColIdentifier i) = pgFmtIdent i + pgFmtJsonbPath' (ColIdentifier i) = fromQt table <> "." <> pgFmtIdent i pgFmtJsonbPath' (KeyIdentifier i) = pgFmtLit i pgFmtJsonbPath' (SingleArrow a b) = pgFmtJsonbPath' a <> "->" <> pgFmtJsonbPath' b diff --git a/test/Feature/InsertSpec.hs b/test/Feature/InsertSpec.hs index 9bb0f6a2d..ea6badbb2 100644 --- a/test/Feature/InsertSpec.hs +++ b/test/Feature/InsertSpec.hs @@ -266,6 +266,12 @@ spec = afterAll_ resetDb $ around withApp $ do liftIO $ simpleHeaders g `shouldSatisfy` matchHeader "Content-Range" "0-9/10" + it "can update based on a computed column" $ + request methodPatch + "/items?always_true=eq.false" + [("Prefer", "return=representation")] + [json| { id: 100 } |] + `shouldRespondWith` 404 it "can provide a representation" $ do _ <- post "/items" [json| { id: 1 } |] diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 670e1e9d0..877b72cb5 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -63,6 +63,10 @@ spec = get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith` "[{\"text_search_vector\":\"'bar':2 'foo':1\"}]" + it "matches with computed column" $ + get "/items?always_true=eq.true" `shouldRespondWith` + "[{\"id\":1},{\"id\":2},{\"id\":3},{\"id\":4},{\"id\":5},{\"id\":6},{\"id\":7},{\"id\":8},{\"id\":9},{\"id\":10},{\"id\":11},{\"id\":12},{\"id\":13},{\"id\":14},{\"id\":15}]" + describe "ordering response" $ do it "by a column asc" $ get "/items?id=lte.2&order=id.asc" diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 9ba10db5a..d5a54adff 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -72,7 +72,6 @@ $$; ALTER FUNCTION postgrest.update_owner() OWNER TO postgrest_test; - CREATE FUNCTION set_authors_only_owner() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -368,6 +367,13 @@ SELECT pg_catalog.setval('articles_id_seq', 1, false); SET search_path = "1", pg_catalog; +CREATE FUNCTION public.always_true("1".items) RETURNS boolean + LANGUAGE sql STABLE + AS $$ SELECT true $$; + +ALTER FUNCTION public.always_true("1".items) OWNER TO postgrest_test; + + ALTER TABLE ONLY authors_only ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret); @@ -557,6 +563,11 @@ REVOKE ALL ON TABLE insertable_view_with_join FROM postgrest_test; GRANT ALL ON TABLE insertable_view_with_join TO postgrest_test; GRANT ALL ON TABLE insertable_view_with_join TO postgrest_anonymous; +REVOKE ALL ON FUNCTION public.always_true("1".items) FROM PUBLIC; +REVOKE ALL ON FUNCTION public.always_true("1".items) FROM postgrest_test; +GRANT ALL ON FUNCTION public.always_true("1".items) TO postgrest_test; +GRANT ALL ON FUNCTION public.always_true("1".items) TO postgrest_anonymous; + SET search_path = postgrest, pg_catalog;