Refactor Field type,move tests to JsonOperatorSpec
This commit is contained in:
committed by
Steve Chávez
parent
100bf494ac
commit
2513c00039
@@ -122,6 +122,7 @@ Test-Suite spec
|
|||||||
, Feature.CorsSpec
|
, Feature.CorsSpec
|
||||||
, Feature.DeleteSpec
|
, Feature.DeleteSpec
|
||||||
, Feature.InsertSpec
|
, Feature.InsertSpec
|
||||||
|
, Feature.JsonOperatorSpec
|
||||||
, Feature.NoJwtSpec
|
, Feature.NoJwtSpec
|
||||||
, Feature.PgVersion96Spec
|
, Feature.PgVersion96Spec
|
||||||
, Feature.ProxySpec
|
, Feature.ProxySpec
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ lexeme p = ws *> p <* ws
|
|||||||
pTreePath :: Parser (EmbedPath, Field)
|
pTreePath :: Parser (EmbedPath, Field)
|
||||||
pTreePath = do
|
pTreePath = do
|
||||||
p <- pFieldName `sepBy1` pDelimiter
|
p <- pFieldName `sepBy1` pDelimiter
|
||||||
jp <- optionMaybe pJsonPath
|
jp <- option [] pJsonPath
|
||||||
return (init p, (last p, jp))
|
return (init p, (last p, jp))
|
||||||
|
|
||||||
pFieldForest :: Parser [Tree SelectItem]
|
pFieldForest :: Parser [Tree SelectItem]
|
||||||
@@ -87,7 +87,7 @@ pJsonPath :: Parser [Text]
|
|||||||
pJsonPath = (<>) <$> many pJsonPathStep <*> ( (:[]) <$> (string "->>" *> pFieldName) )
|
pJsonPath = (<>) <$> many pJsonPathStep <*> ( (:[]) <$> (string "->>" *> pFieldName) )
|
||||||
|
|
||||||
pField :: Parser Field
|
pField :: Parser Field
|
||||||
pField = lexeme $ (,) <$> pFieldName <*> optionMaybe pJsonPath
|
pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath
|
||||||
|
|
||||||
aliasSeparator :: Parser ()
|
aliasSeparator :: Parser ()
|
||||||
aliasSeparator = char ':' >> notFollowedBy (char ':')
|
aliasSeparator = char ':' >> notFollowedBy (char ':')
|
||||||
@@ -112,7 +112,7 @@ pFieldSelect = lexeme $
|
|||||||
)
|
)
|
||||||
<|> do
|
<|> do
|
||||||
s <- pStar
|
s <- pStar
|
||||||
return ((s, Nothing), Nothing, Nothing, Nothing)
|
return ((s, []), Nothing, Nothing, Nothing)
|
||||||
|
|
||||||
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
||||||
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
|
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
|
||||||
|
|||||||
@@ -447,14 +447,14 @@ pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <>
|
|||||||
where notOp = if hasNot then "NOT" else ""
|
where notOp = if hasNot then "NOT" else ""
|
||||||
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
|
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
|
||||||
|
|
||||||
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
|
pgFmtJsonPath :: JsonPath -> SqlFragment
|
||||||
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
|
pgFmtJsonPath [] = ""
|
||||||
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
|
pgFmtJsonPath [x] = "->>" <> pgFmtLit x
|
||||||
pgFmtJsonPath _ = ""
|
pgFmtJsonPath (x:xs) = "->" <> pgFmtLit x <> pgFmtJsonPath xs
|
||||||
|
|
||||||
pgFmtAs :: Maybe JsonPath -> Maybe Alias -> SqlFragment
|
pgFmtAs :: JsonPath -> Maybe Alias -> SqlFragment
|
||||||
pgFmtAs Nothing Nothing = ""
|
pgFmtAs [] Nothing = ""
|
||||||
pgFmtAs (Just xx) Nothing = case lastMay xx of
|
pgFmtAs jp Nothing = case lastMay jp of
|
||||||
Just alias -> " AS " <> pgFmtIdent alias
|
Just alias -> " AS " <> pgFmtIdent alias
|
||||||
Nothing -> ""
|
Nothing -> ""
|
||||||
pgFmtAs _ (Just alias) = " AS " <> pgFmtIdent alias
|
pgFmtAs _ (Just alias) = " AS " <> pgFmtIdent alias
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Sh
|
|||||||
|
|
||||||
type FieldName = Text
|
type FieldName = Text
|
||||||
type JsonPath = [Text]
|
type JsonPath = [Text]
|
||||||
type Field = (FieldName, Maybe JsonPath)
|
type Field = (FieldName, JsonPath)
|
||||||
type Alias = Text
|
type Alias = Text
|
||||||
type Cast = Text
|
type Cast = Text
|
||||||
type NodeName = Text
|
type NodeName = Text
|
||||||
|
|||||||
@@ -193,10 +193,6 @@ spec =
|
|||||||
it "can query columns that begin with and/or reserved words" $
|
it "can query columns that begin with and/or reserved words" $
|
||||||
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
||||||
|
|
||||||
it "can query jsonb columns" $
|
|
||||||
get "/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id" `shouldRespondWith`
|
|
||||||
[json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "fails when using IN without () and provides meaningful error message" $
|
it "fails when using IN without () and provides meaningful error message" $
|
||||||
get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
|
get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
|
||||||
[json|{
|
[json|{
|
||||||
|
|||||||
@@ -390,16 +390,6 @@ spec = do
|
|||||||
[json| [{ a: "keepme", b: null }] |]
|
[json| [{ a: "keepme", b: null }] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can set a json column to escaped value" $ do
|
|
||||||
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
|
|
||||||
request methodPatch "/json?data->>escaped=eq.bar"
|
|
||||||
[("Prefer", "return=representation")]
|
|
||||||
[json| { "data": { "escaped":" \"bar" } } |]
|
|
||||||
`shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = []
|
|
||||||
}
|
|
||||||
|
|
||||||
it "can update based on a computed column" $
|
it "can update based on a computed column" $
|
||||||
request methodPatch
|
request methodPatch
|
||||||
"/items?always_true=eq.false"
|
"/items?always_true=eq.false"
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
module Feature.JsonOperatorSpec where
|
||||||
|
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Network.HTTP.Types
|
||||||
|
|
||||||
|
import SpecHelper
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import Protolude hiding (get)
|
||||||
|
|
||||||
|
spec :: SpecWith Application
|
||||||
|
spec = describe "json and jsonb operators" $ do
|
||||||
|
context "Shaping response with select parameter" $ do
|
||||||
|
it "obtains a json subfield one level with casting" $
|
||||||
|
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"
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "renames json subfield one level with casting" $
|
||||||
|
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
|
||||||
|
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "fails on bad casting (data of the wrong format)" $
|
||||||
|
get "/complex_items?select=settings->foo->>bar::integer"
|
||||||
|
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
|
||||||
|
{ matchStatus = 400 , matchHeaders = [] }
|
||||||
|
|
||||||
|
it "obtains a json subfield two levels (string)" $
|
||||||
|
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
|
||||||
|
[json| [{"bar":"baz"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "renames json subfield two levels (string)" $
|
||||||
|
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
|
||||||
|
[json| [{"myBar":"baz"}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "obtains a 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
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "renames json subfield two levels with casting (int)" $
|
||||||
|
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
|
||||||
|
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "filtering response" $ do
|
||||||
|
it "can filter by properties inside json column" $ do
|
||||||
|
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
|
||||||
|
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
|
||||||
|
[json| [] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can filter by properties inside json column using not" $
|
||||||
|
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
|
||||||
|
[json| [] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can filter by properties inside json column using ->>" $
|
||||||
|
get "/json?data->>id=eq.1" `shouldRespondWith`
|
||||||
|
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can be filtered with and/or" $
|
||||||
|
get "/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id" `shouldRespondWith`
|
||||||
|
[json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "ordering response" $ do
|
||||||
|
it "orders by a json column property asc" $
|
||||||
|
get "/json?order=data->>id.asc" `shouldRespondWith`
|
||||||
|
[json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "orders by a json column with two level property nulls first" $
|
||||||
|
get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith`
|
||||||
|
[json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "Patching record, in a nonempty table" $ do
|
||||||
|
it "can set a json column to escaped value" $ do
|
||||||
|
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
|
||||||
|
request methodPatch "/json?data->>escaped=eq.bar"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json| { "data": { "escaped":" \"bar" } } |]
|
||||||
|
`shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |]
|
||||||
|
{ matchStatus = 200 , matchHeaders = [] }
|
||||||
@@ -216,23 +216,6 @@ spec = do
|
|||||||
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
|
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
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"
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "rename json subfield one level with casting (json)" $
|
|
||||||
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
|
|
||||||
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "fails on bad casting (data of the wrong format)" $
|
|
||||||
get "/complex_items?select=settings->foo->>bar::integer"
|
|
||||||
`shouldRespondWith` [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)" $
|
it "fails on bad casting (wrong cast type)" $
|
||||||
get "/complex_items?select=id::fakecolumntype"
|
get "/complex_items?select=id::fakecolumntype"
|
||||||
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
|
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
|
||||||
@@ -241,27 +224,6 @@ spec = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
it "json subfield two levels (string)" $
|
|
||||||
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
|
|
||||||
[json| [{"bar":"baz"}] |]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "rename json subfield two levels (string)" $
|
|
||||||
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
|
|
||||||
[json| [{"myBar":"baz"}] |]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "rename json subfield two levels with casting (int)" $
|
|
||||||
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
|
|
||||||
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "requesting parents and children" $
|
it "requesting parents and children" $
|
||||||
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
||||||
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||||
@@ -630,16 +592,6 @@ spec = do
|
|||||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "by a json column property asc" $
|
|
||||||
get "/json?order=data->>id.asc" `shouldRespondWith`
|
|
||||||
[json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "by a json column with two level property nulls first" $
|
|
||||||
get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith`
|
|
||||||
[json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "without other constraints" $
|
it "without other constraints" $
|
||||||
get "/items?order=id.asc" `shouldRespondWith` 200
|
get "/items?order=id.asc" `shouldRespondWith` 200
|
||||||
|
|
||||||
@@ -771,22 +723,6 @@ spec = do
|
|||||||
respHeaders `shouldSatisfy` matchHeader
|
respHeaders `shouldSatisfy` matchHeader
|
||||||
"Content-Location" "/simple_pk"
|
"Content-Location" "/simple_pk"
|
||||||
|
|
||||||
describe "jsonb" $ do
|
|
||||||
it "can filter by properties inside json column" $ do
|
|
||||||
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
|
|
||||||
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
|
|
||||||
[json| [] |]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
it "can filter by properties inside json column using not" $
|
|
||||||
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
|
|
||||||
[json| [] |]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
it "can filter by properties inside json column using ->>" $
|
|
||||||
get "/json?data->>id=eq.1" `shouldRespondWith`
|
|
||||||
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
describe "weird requests" $ do
|
describe "weird requests" $ do
|
||||||
it "can query as normal" $ do
|
it "can query as normal" $ do
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ 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
|
||||||
|
import qualified Feature.JsonOperatorSpec
|
||||||
import qualified Feature.NoJwtSpec
|
import qualified Feature.NoJwtSpec
|
||||||
import qualified Feature.QueryLimitedSpec
|
import qualified Feature.QueryLimitedSpec
|
||||||
import qualified Feature.QuerySpec
|
import qualified Feature.QuerySpec
|
||||||
@@ -77,6 +78,7 @@ main = do
|
|||||||
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
||||||
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
|
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
|
||||||
, ("Feature.InsertSpec" , Feature.InsertSpec.spec)
|
, ("Feature.InsertSpec" , Feature.InsertSpec.spec)
|
||||||
|
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec)
|
||||||
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
|
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
|
||||||
, ("Feature.RpcSpec" , Feature.RpcSpec.spec)
|
, ("Feature.RpcSpec" , Feature.RpcSpec.spec)
|
||||||
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
|
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
|
||||||
|
|||||||
Reference in New Issue
Block a user