From d34afe861a425880d78e7c12ac6442d1bf700169 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 12 Jun 2018 16:09:51 -0500 Subject: [PATCH] Allow finishing a json path with single arrow `->` Now a json can be obtained without resorting to casting - Previously: `/json_arr?select=data->>2::json` - Now: `/json_arr?select=data->2` --- CHANGELOG.md | 1 + src/PostgREST/Parsers.hs | 23 +++++++++++++++-------- src/PostgREST/QueryBuilder.hs | 14 +++++++------- src/PostgREST/Types.hs | 11 +++++++---- test/Feature/JsonOperatorSpec.hs | 31 +++++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f345af02f..1e692933d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - #1099, Numbers in json path `?select=data->1->>key` now get treated as json array indexes instead of keys - @steve-chavez +- #1128, Allow finishing a json path with a single arrow `->`. Now a json can be obtained without resorting to casting, Previously: `/json_arr?select=data->>2::json`, now: `/json_arr?select=data->2` - @steve-chavez ## [0.5.0.0] - 2018-05-14 diff --git a/src/PostgREST/Parsers.hs b/src/PostgREST/Parsers.hs index 724d0b5a5..7e682f1ea 100644 --- a/src/PostgREST/Parsers.hs +++ b/src/PostgREST/Parsers.hs @@ -81,15 +81,22 @@ pFieldName = do dash = isDash *> pure '-' pJsonPath :: Parser JsonPath -pJsonPath = (<>) <$> many pJsonPathOp <*> ( (:[]) <$> (string "->>" *> (try pJIdx <|> pJKey)) ) +pJsonPath = many pJsonOperation where - pJsonPathOp :: Parser JsonPathOp - pJsonPathOp = try (string "->" *> pJIdx) <|> try (string "->" *> pJKey) - pJKey = JKey . toS <$> pFieldName - pJIdx = JIdx . toS <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd - pEnd = try (void $ lookAhead (string "->")) <|> - try (void $ lookAhead (string "::")) <|> - try eof + pJsonOperation :: Parser JsonOperation + pJsonOperation = pJsonArrow <*> pJsonOperand + + pJsonArrow = + try (string "->>" $> J2Arrow) <|> + try (string "->" $> JArrow) + + pJsonOperand = + let pJKey = JKey . toS <$> pFieldName + pJIdx = JIdx . toS <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd + pEnd = try (void $ lookAhead (string "->")) <|> + try (void $ lookAhead (string "::")) <|> + try eof in + try pJIdx <|> try pJKey pField :: Parser Field pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 91fdf2a2c..8045573bc 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -450,22 +450,22 @@ pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt pgFmtJsonPath :: JsonPath -> SqlFragment pgFmtJsonPath = \case - [] -> "" - [x] -> "->>" <> pgFmtJsonPathOp x - (x:xs) -> "->" <> pgFmtJsonPathOp x <> pgFmtJsonPath xs + [] -> "" + (JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs + (J2Arrow x:xs) -> "->>" <> pgFmtJsonOperand x <> pgFmtJsonPath xs where - pgFmtJsonPathOp (JKey k) = pgFmtLit k - pgFmtJsonPathOp (JIdx i) = pgFmtLit i <> "::int" + pgFmtJsonOperand (JKey k) = pgFmtLit k + pgFmtJsonOperand (JIdx i) = pgFmtLit i <> "::int" pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment pgFmtAs _ [] Nothing = "" -pgFmtAs fName jp Nothing = case lastMay jp of +pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of Just (JKey key) -> " AS " <> pgFmtIdent key Just (JIdx _) -> " AS " <> pgFmtIdent (fromMaybe fName lastKey) -- We get the lastKey because on: -- `select=data->1->mycol->>2`, we need to show the result as [ {"mycol": ..}, {"mycol": ..} ] -- `select=data->3`, we need to show the result as [ {"data": ..}, {"data": ..} ] - where lastKey = jpOp <$> find (\case JKey{} -> True; _ -> False) (reverse jp) + where lastKey = jVal <$> find (\case JKey{} -> True; _ -> False) (jOp <$> reverse jp) Nothing -> "" pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 375cd4f00..928ebc7d7 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -238,12 +238,15 @@ instance Show LogicOperator where data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Show, Eq) type FieldName = Text -type JsonPath = [JsonPathOp] {-| - Json path operands as specified in https://www.postgresql.org/docs/9.5/static/functions-json.html - the array index is Text because we reuse our escaping functons and let pg do the casting with '1'::int + Json path operations as specified in https://www.postgresql.org/docs/9.4/static/functions-json.html -} -data JsonPathOp = JKey{jpOp :: Text} | JIdx{jpOp :: Text} deriving (Show, Eq) +type JsonPath = [JsonOperation] +-- | Represents the single arrow `->` or double arrow `->>` operators +data JsonOperation = JArrow{jOp :: JsonOperand} | J2Arrow{jOp :: JsonOperand} deriving (Show, Eq) +-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text because we reuse our escaping functons and let pg do the casting with '1'::int +data JsonOperand = JKey{jVal :: Text} | JIdx{jVal :: Text} deriving (Show, Eq) + type Field = (FieldName, JsonPath) type Alias = Text type Cast = Text diff --git a/test/Feature/JsonOperatorSpec.hs b/test/Feature/JsonOperatorSpec.hs index 20328eec0..cbe1055c3 100644 --- a/test/Feature/JsonOperatorSpec.hs +++ b/test/Feature/JsonOperatorSpec.hs @@ -48,6 +48,20 @@ spec = describe "json and jsonb operators" $ do [json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now { matchHeaders = [matchContentTypeJson] } + -- TODO the status code for the error is 404, this is because 42883 represents undefined function + -- this works fine for /rpc/unexistent requests, but for this case a 500 seems more appropriate + it "fails when a double arrow ->> is followed with a single arrow ->" $ do + get "/json_arr?select=data->>c->1" + `shouldRespondWith` [json| + {"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.", + "details":null,"code":"42883","message":"operator does not exist: text -> integer"} |] + { matchStatus = 404 , matchHeaders = [] } + get "/json_arr?select=data->>c->b" + `shouldRespondWith` [json| + {"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.", + "details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |] + { matchStatus = 404 , matchHeaders = [] } + context "with array index" $ do it "can get array of ints and alias/cast it" $ do get "/json_arr?select=data->>0::int&id=in.(1,2)" `shouldRespondWith` @@ -86,6 +100,23 @@ spec = describe "json and jsonb operators" $ do [json| [{"xy-6":3}] |] { matchHeaders = [matchContentTypeJson] } + context "finishing json path with single arrow ->" $ do + it "works when finishing with a key" $ do + get "/json_arr?select=data->c&id=in.(7,8)" `shouldRespondWith` + [json| [{"c":[1,2,3]}, {"c":[{"d": [4,5,6,7,8]}]}] |] + { matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith` + [json| [{"a":"A"}, {"a":[1,2,3]}] |] + { matchHeaders = [matchContentTypeJson] } + + it "works when finishing with an index" $ do + get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith` + [json| [{"a":"A"}, {"a":[1,2,3]}] |] + { matchHeaders = [matchContentTypeJson] } + get "/json_arr?select=data->c->0->d&id=eq.8" `shouldRespondWith` + [json| [{"d":[4,5,6,7,8]}] |] + { matchHeaders = [matchContentTypeJson] } + context "filtering response" $ do it "can filter by properties inside json column" $ do get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`