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`
This commit is contained in:
committed by
Steve Chávez
parent
30dfadec7b
commit
d34afe861a
@@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- #1099, Numbers in json path `?select=data->1->>key` now get treated as json array indexes instead of keys - @steve-chavez
|
- #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
|
## [0.5.0.0] - 2018-05-14
|
||||||
|
|
||||||
|
|||||||
@@ -81,15 +81,22 @@ pFieldName = do
|
|||||||
dash = isDash *> pure '-'
|
dash = isDash *> pure '-'
|
||||||
|
|
||||||
pJsonPath :: Parser JsonPath
|
pJsonPath :: Parser JsonPath
|
||||||
pJsonPath = (<>) <$> many pJsonPathOp <*> ( (:[]) <$> (string "->>" *> (try pJIdx <|> pJKey)) )
|
pJsonPath = many pJsonOperation
|
||||||
where
|
where
|
||||||
pJsonPathOp :: Parser JsonPathOp
|
pJsonOperation :: Parser JsonOperation
|
||||||
pJsonPathOp = try (string "->" *> pJIdx) <|> try (string "->" *> pJKey)
|
pJsonOperation = pJsonArrow <*> pJsonOperand
|
||||||
pJKey = JKey . toS <$> pFieldName
|
|
||||||
pJIdx = JIdx . toS <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd
|
pJsonArrow =
|
||||||
pEnd = try (void $ lookAhead (string "->")) <|>
|
try (string "->>" $> J2Arrow) <|>
|
||||||
try (void $ lookAhead (string "::")) <|>
|
try (string "->" $> JArrow)
|
||||||
try eof
|
|
||||||
|
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 :: Parser Field
|
||||||
pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath
|
pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath
|
||||||
|
|||||||
@@ -450,22 +450,22 @@ pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
|
|||||||
|
|
||||||
pgFmtJsonPath :: JsonPath -> SqlFragment
|
pgFmtJsonPath :: JsonPath -> SqlFragment
|
||||||
pgFmtJsonPath = \case
|
pgFmtJsonPath = \case
|
||||||
[] -> ""
|
[] -> ""
|
||||||
[x] -> "->>" <> pgFmtJsonPathOp x
|
(JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
|
||||||
(x:xs) -> "->" <> pgFmtJsonPathOp x <> pgFmtJsonPath xs
|
(J2Arrow x:xs) -> "->>" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
|
||||||
where
|
where
|
||||||
pgFmtJsonPathOp (JKey k) = pgFmtLit k
|
pgFmtJsonOperand (JKey k) = pgFmtLit k
|
||||||
pgFmtJsonPathOp (JIdx i) = pgFmtLit i <> "::int"
|
pgFmtJsonOperand (JIdx i) = pgFmtLit i <> "::int"
|
||||||
|
|
||||||
pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment
|
pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment
|
||||||
pgFmtAs _ [] Nothing = ""
|
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 (JKey key) -> " AS " <> pgFmtIdent key
|
||||||
Just (JIdx _) -> " AS " <> pgFmtIdent (fromMaybe fName lastKey)
|
Just (JIdx _) -> " AS " <> pgFmtIdent (fromMaybe fName lastKey)
|
||||||
-- We get the lastKey because on:
|
-- We get the lastKey because on:
|
||||||
-- `select=data->1->mycol->>2`, we need to show the result as [ {"mycol": ..}, {"mycol": ..} ]
|
-- `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": ..} ]
|
-- `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 -> ""
|
Nothing -> ""
|
||||||
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
|
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
|
||||||
|
|
||||||
|
|||||||
@@ -238,12 +238,15 @@ instance Show LogicOperator where
|
|||||||
data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Show, Eq)
|
data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Show, Eq)
|
||||||
|
|
||||||
type FieldName = Text
|
type FieldName = Text
|
||||||
type JsonPath = [JsonPathOp]
|
|
||||||
{-|
|
{-|
|
||||||
Json path operands as specified in https://www.postgresql.org/docs/9.5/static/functions-json.html
|
Json path operations as specified in https://www.postgresql.org/docs/9.4/static/functions-json.html
|
||||||
the array index is Text because we reuse our escaping functons and let pg do the casting with '1'::int
|
|
||||||
-}
|
-}
|
||||||
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 Field = (FieldName, JsonPath)
|
||||||
type Alias = Text
|
type Alias = Text
|
||||||
type Cast = Text
|
type Cast = Text
|
||||||
|
|||||||
@@ -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
|
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ 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
|
context "with array index" $ do
|
||||||
it "can get array of ints and alias/cast it" $ do
|
it "can get array of ints and alias/cast it" $ do
|
||||||
get "/json_arr?select=data->>0::int&id=in.(1,2)" `shouldRespondWith`
|
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}] |]
|
[json| [{"xy-6":3}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ 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
|
context "filtering response" $ do
|
||||||
it "can filter by properties inside json column" $ do
|
it "can filter by properties inside json column" $ do
|
||||||
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
|
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
|
||||||
|
|||||||
Reference in New Issue
Block a user