From c13f0a369b8f0a5fd926bfa9cd8737db9f65bb2e Mon Sep 17 00:00:00 2001 From: Ruslan Talpa Date: Tue, 10 May 2016 10:58:57 +0300 Subject: [PATCH] Support node/column renaming #310 --- CHANGELOG.md | 4 +-- docs/api/reading.md | 25 +++++++++++++-- src/PostgREST/App.hs | 58 +++++++++++++++++------------------ src/PostgREST/Parsers.hs | 38 ++++++++++++++++------- src/PostgREST/QueryBuilder.hs | 51 +++++++++++++++--------------- src/PostgREST/Types.hs | 5 +-- test/Feature/QuerySpec.hs | 32 ++++++++++++++++++- 7 files changed, 138 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd05825c9..2e2a0e972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,11 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Added - - Reload database schema on SIGHUP - @begriffs - Support "-" in column names - @ruslantalpa +- Support column/node renaming `alias:column` - @ruslantalpa ### Fixed -- Set transaction mode to READ when possible to support connecting to read replicas - @ruslantalpa - - Omit Content-Type header for empty body - @begriffs - Prevent role from being changed twice - @begriffs - Use read-only transaction for read requests - @ruslantalpa diff --git a/docs/api/reading.md b/docs/api/reading.md index 87f99c293..feb6d5e4b 100644 --- a/docs/api/reading.md +++ b/docs/api/reading.md @@ -258,7 +258,8 @@ but the the select query is recursive. You could for instance specify GET /foo?select=x, y, bar{z, w, baz{*}} ``` -You can select not only using table names, but also column names! +You can select not only using table names, but also foreign key column names! +This is especially needed when you have a table with two foreign keys pointing to the same table, for example billing_address_id and shipping_address_id. To embed the same foreign key row from our client example earlier you could do the following: @@ -270,8 +271,7 @@ In the response there will be a `client_id` object containing all the data for that row. However, a `client_id` object doesn't make a lot of sense, so you -could do one of two things. Create a view which renames `client_id` -to just `client` (this is the hard way), or just try `client{*}` +could do one of two things. Tell PostgREST that you want the key renamed by using the `alias` feature like so `client:client_id{*}`, or just try `client{*}` in the select parameter! PostgREST supports smart ducktype checking for common foreign key names, so if your column name ends with `_id`, `_fk`, or any variation of the two (including camelcase) @@ -285,6 +285,25 @@ GET /projects?id=eq.1&select=id, name, client{*} Would embed in the `client` key the row referenced with `client_id`. +The `alias` feature works for embedded entities and also for regular columns. This is useful in situations where for example you use different naming conventions in the database and frontend. + +The following request will produce the output below: +```HTTP +GET /orders?id=eq.1&select=orderId:id, customer:customer_id{customerId:id, customerName:name} +``` + +```json +[ + { + "orderId": 1, + "customer": { + "customerId": 1, + "customerName": "John Smith" + } + } +] +``` +

Design Consideration

In order for this feature to work as expected after a schema change, PostgREST currently requires to be restarted.

diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 54156f53b..b3355e715 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -7,12 +7,10 @@ module PostgREST.App ( ) where import Control.Applicative -import Control.Arrow ((***)) -import Control.Monad (join) import Data.Bifunctor (first) -import Data.List (find, sortBy, delete) +import Data.IORef (IORef, readIORef) +import Data.List (find, delete) import Data.Maybe (isJust, fromMaybe, fromJust, mapMaybe) -import Data.Ord (comparing) import Data.Ranged.Ranges (emptyRange) import Data.String.Conversions (cs) import Data.Text (Text, replace, strip) @@ -24,10 +22,8 @@ import qualified Hasql.Transaction as HT import Text.Parsec.Error import Text.ParserCombinators.Parsec (parse) -import Network.HTTP.Base (urlEncodeVars) import Network.HTTP.Types.Header import Network.HTTP.Types.Status -import Network.HTTP.Types.URI (parseSimpleQuery) import Network.Wai import Network.Wai.Middleware.RequestLogger (logStdout) @@ -64,29 +60,31 @@ import PostgREST.Types import Prelude -transactionMode :: Action -> H.Mode -transactionMode ActionRead = HT.Read -transactionMode ActionInfo = HT.Read -transactionMode _ = HT.Write - -postgrest :: AppConfig -> DbStructure -> P.Pool -> Application -postgrest conf dbStructure pool = +postgrest :: AppConfig -> IORef DbStructure -> P.Pool -> Application +postgrest conf refDbStructure pool = let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in middle $ \ req respond -> do time <- getPOSIXTime body <- strictRequestBody req + dbStructure <- readIORef refDbStructure let schema = cs $ configSchema conf apiRequest = userApiRequest schema req body - handleReq = runWithClaims conf time (app dbStructure conf apiRequest) req + handleReq = runWithClaims conf time (app dbStructure conf) apiRequest + txMode = transactionMode $ iAction apiRequest resp <- either pgErrResponse id <$> P.use pool - (HT.run handleReq HT.ReadCommitted (transactionMode $ iAction apiRequest)) + (HT.run handleReq HT.ReadCommitted txMode) respond resp -app :: DbStructure -> AppConfig -> ApiRequest -> Request -> H.Transaction Response -app dbStructure conf apiRequest req = +transactionMode :: Action -> H.Mode +transactionMode ActionRead = HT.Read +transactionMode ActionInfo = HT.Read +transactionMode _ = HT.Write + +app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response +app dbStructure conf apiRequest = let -- TODO: blow up for Left values (there is a middleware that checks the headers) contentType = either (const ApplicationJSON) id (iAccepts apiRequest) @@ -110,11 +108,7 @@ app dbStructure conf apiRequest req = else responseLBS status200 [contentTypeH] (cs body) else do let (status, contentRange) = rangeHeader queryTotal tableTotal - canonical = urlEncodeVars -- should this be moved to the dbStructure (location)? - . sortBy (comparing fst) - . map (join (***) cs) - . parseSimpleQuery - $ rawQueryString req + canonical = iCanonicalQS apiRequest return $ responseLBS status [contentTypeH, contentRange, ("Content-Location", @@ -133,12 +127,14 @@ app dbStructure conf apiRequest req = let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == TextCSV) payload row <- H.query uniform stm let (_, _, location, body) = extractQueryResult row - return $ responseLBS status201 - [ - contentTypeH, - (hLocation, "/" <> cs table <> "?" <> cs location) - ] - $ if iPreferRepresentation apiRequest == Full then cs body else "" + + return $ if iPreferRepresentation apiRequest == Full + then responseLBS status201 [ + contentTypeH, + (hLocation, "/" <> cs table <> "?" <> cs location) + ] (cs body) + else responseLBS status201 + [(hLocation, "/" <> cs table <> "?" <> cs location)] "" (ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON uniform)) -> case mutateSqlParts of @@ -151,8 +147,9 @@ app dbStructure conf apiRequest req = s = case () of _ | queryTotal == 0 -> status404 | iPreferRepresentation apiRequest == Full -> status200 | otherwise -> status204 - return $ responseLBS s [contentTypeH, r] - $ if iPreferRepresentation apiRequest == Full then cs body else "" + return $ if iPreferRepresentation apiRequest == Full + then responseLBS s [contentTypeH, r] (cs body) + else responseLBS s [r] "" (ActionDelete, TargetIdent qi, Nothing) -> case mutateSqlParts of @@ -346,6 +343,7 @@ addFilter (path, flt) (Node rn forest) = maybeNode = find fnd forst where fnd :: ReadRequest -> Bool + fnd (Node (_,(n,_,_)) _) = n == name -- in a relation where one of the tables mathces "TableName" -- replace the name to that table with pg_source diff --git a/src/PostgREST/Parsers.hs b/src/PostgREST/Parsers.hs index a887675fd..5c5c2c31f 100644 --- a/src/PostgREST/Parsers.hs +++ b/src/PostgREST/Parsers.hs @@ -12,16 +12,21 @@ import PostgREST.QueryBuilder (operators) import PostgREST.Types import Text.ParserCombinators.Parsec hiding (many, (<|>)) + pRequestSelect :: Text -> Parser ReadRequest pRequestSelect rootNodeName = do fieldTree <- pFieldForest - return $ foldr treeEntry (Node (Select [] [rootNodeName] [] Nothing, (rootNodeName, Nothing)) []) fieldTree + return $ foldr treeEntry (Node (readQuery, (rootNodeName, Nothing, Nothing)) []) fieldTree where + readQuery = Select [] [rootNodeName] [] Nothing treeEntry :: Tree SelectItem -> ReadRequest -> ReadRequest - treeEntry (Node fld@((fn, _),_) fldForest) (Node (q, i) rForest) = + treeEntry (Node fld@((fn, _),_,alias) fldForest) (Node (q, i) rForest) = case fldForest of [] -> Node (q {select=fld:select q}, i) rForest - _ -> Node (q, i) (foldr treeEntry (Node (Select [] [fn] [] Nothing, (fn, Nothing)) []) fldForest:rForest) + _ -> Node (q, i) newForest + where + newForest = + foldr treeEntry (Node (Select [] [fn] [] Nothing, (fn, Nothing, alias)) []) fldForest:rForest pRequestFilter :: (String, String) -> Either ParseError (Path, Filter) pRequestFilter (k, v) = (,) <$> path <*> (Filter <$> fld <*> op <*> val) @@ -51,24 +56,20 @@ pFieldForest :: Parser [Tree SelectItem] pFieldForest = pFieldTree `sepBy1` lexeme (char ',') pFieldTree :: Parser (Tree SelectItem) -pFieldTree = try (Node <$> pSelect <*> between (char '{') (char '}') pFieldForest) +pFieldTree = try (Node <$> pSimpleSelect <*> between (char '{') (char '}') pFieldForest) <|> Node <$> pSelect <*> pure [] pStar :: Parser Text pStar = cs <$> (string "*" *> pure ("*"::String)) - pFieldName :: Parser Text pFieldName = do matches <- (many1 (letter <|> digit <|> oneOf "_") `sepBy1` dash) "field name (* or [a..z0..9_])" return $ intercalate "-" $ map cs matches where isDash :: GenParser Char st () - isDash = try (do{ - _ <- char '-' - ; notFollowedBy (char '>') - }) + isDash = try ( char '-' >> notFollowedBy (char '>') ) dash :: Parser Char dash = isDash *> pure '-' @@ -82,12 +83,27 @@ pJsonPath = (++) <$> many pJsonPathStep <*> ( (:[]) <$> (string "->>" *> pFieldN pField :: Parser Field pField = lexeme $ (,) <$> pFieldName <*> optionMaybe pJsonPath +aliasSeparator = char ':' >> notFollowedBy (char ':') + +pSimpleSelect :: Parser SelectItem +pSimpleSelect = lexeme $ try ( do + alias <- optionMaybe ( try(pFieldName <* aliasSeparator) ) + fld <- pField + return (fld, Nothing, alias) + ) + pSelect :: Parser SelectItem pSelect = lexeme $ - try ((,) <$> pField <*>((cs <$>) <$> optionMaybe (string "::" *> many letter)) ) + try ( + do + alias <- optionMaybe ( try(pFieldName <* aliasSeparator) ) + fld <- pField + cast <- optionMaybe (string "::" *> many letter) + return (fld, cs <$> cast, alias) + ) <|> do s <- pStar - return ((s, Nothing), Nothing) + return ((s, Nothing), Nothing, Nothing) pOperator :: Parser Operator pOperator = cs <$> (pOp "operator (eq, gt, ...)") diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 61e05495b..5f2b0c9f4 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -158,18 +158,18 @@ createWriteStatement qi selectQuery mutateQuery isSingle Full | otherwise = asJsonF addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest -addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _)) forest) = +addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _, alias)) forest) = case parentNode of - (Just (Node (Select{from=[parentTable]}, (_, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest + (Just (Node (Select{from=[parentTable]}, (_, _, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest where rel = note ("no relation between " <> parentTable <> " and " <> name) $ findRelationByTable schema name parentTable <|> findRelationByColumn schema parentTable name - addRel :: (ReadQuery, (NodeName, Maybe Relation)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation)) - addRel (query', (n, _)) r = (query' {from=fromRelation}, (n, Just r)) + addRel :: (ReadQuery, (NodeName, Maybe Relation, Maybe Alias)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation, Maybe Alias)) + addRel (query', (n, _, a)) r = (query' {from=fromRelation}, (n, Just r, a)) where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from query') - _ -> Node (query, (name, Nothing)) <$> updatedForest + _ -> Node (query, (name, Nothing, alias)) <$> updatedForest where updatedForest = mapM (addRelations schema allRelations (Just node)) forest -- Searches through all the relations and returns a match given the parameter conditions. @@ -182,13 +182,13 @@ addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _ where n `colMatches` rc = (cs ("^" <> rc <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (cs n :: BS.ByteString) addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest -addJoinConditions schema (Node (query, (n, r)) forest) = +addJoinConditions schema (Node (query, (n, r, a)) forest) = case r of - Nothing -> Node (updatedQuery, (n,r)) <$> updatedForest -- this is the root node - Just rel@Relation{relType=Child} -> Node (addCond updatedQuery (getJoinConditions rel),(n,r)) <$> updatedForest - Just Relation{relType=Parent} -> Node (updatedQuery, (n,r)) <$> updatedForest + Nothing -> Node (updatedQuery, (n,r,a)) <$> updatedForest -- this is the root node + Just rel@Relation{relType=Child} -> Node (addCond updatedQuery (getJoinConditions rel),(n,r,a)) <$> updatedForest + Just Relation{relType=Parent} -> Node (updatedQuery, (n,r,a)) <$> updatedForest Just rel@Relation{relType=Many, relLTable=(Just linkTable)} -> - Node (qq, (n, r)) <$> updatedForest + Node (qq, (n, r, a)) <$> updatedForest where query' = addCond updatedQuery (getJoinConditions rel) qq = query'{from=tableName linkTable : from query'} @@ -199,7 +199,7 @@ addJoinConditions schema (Node (query, (n, r)) forest) = where parentJoinConditions = map (getJoinConditions . snd) parents parents = mapMaybe (getParents . rootLabel) forest - getParents (_, (tbl, Just rel@Relation{relType=Parent})) = Just (tbl, rel) + getParents (_, (tbl, Just rel@Relation{relType=Parent}, _)) = Just (tbl, rel) getParents _ = Nothing updatedForest = mapM (addJoinConditions schema) forest addCond query' con = query'{flt_=con ++ flt_ query'} @@ -262,7 +262,7 @@ pgFmtLit x = requestToCountQuery :: Schema -> DbRequest -> SqlQuery requestToCountQuery _ (DbMutate _) = undefined -requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _)) _)) = +requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _, _)) _)) = unwords [ "SELECT pg_catalog.count(1)", "FROM ", fromQi $ QualifiedIdentifier schema mainTbl, @@ -276,7 +276,7 @@ requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _)) requestToQuery :: Schema -> DbRequest -> SqlQuery requestToQuery _ (DbMutate (Insert _ (PayloadParseError _))) = undefined requestToQuery _ (DbMutate (Update _ (PayloadParseError _) _)) = undefined -requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (nodeName, maybeRelation)) forest)) = +requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (nodeName, maybeRelation, _)) forest)) = query where -- TODO! the folloing helper functions are just to remove the "schema" part when the table is "source" which is the name @@ -315,29 +315,29 @@ requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (nod filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" t) _)) = parentTable == t filterParentConditions _ _ = False getQueryParts :: Tree ReadNode -> ([(SqlFragment, TableName)], [SqlFragment]) -> ([(SqlFragment,TableName)], [SqlFragment]) - getQueryParts (Node n@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}})) forst) (j,s) = (j,sel:s) + getQueryParts (Node n@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (j,sel:s) where sel = "COALESCE((" <> "SELECT array_to_json(array_agg(row_to_json("<>pgFmtIdent table<>"))) " <> "FROM (" <> subquery <> ") " <> pgFmtIdent table - <> "), '[]') AS " <> pgFmtIdent name + <> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) where subquery = requestToQuery schema (DbRead (Node n forst)) - getQueryParts (Node n@(_, (name, Just Relation{relType=Parent,relTable=Table{tableName=table}})) forst) (j,s) = (joi:j,sel:s) + getQueryParts (Node n@(_, (name, Just Relation{relType=Parent,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (joi:j,sel:s) where - sel = "row_to_json(" <> pgFmtIdent table <> ".*) AS "<>pgFmtIdent name --TODO must be singular + sel = "row_to_json(" <> pgFmtIdent table <> ".*) AS " <> pgFmtIdent (fromMaybe name alias) joi = ("( " <> subquery <> " ) AS " <> pgFmtIdent table, table) where subquery = requestToQuery schema (DbRead (Node n forst)) - getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}})) forst) (j,s) = (j,sel:s) + getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}}, alias)) forst) (j,s) = (j,sel:s) where sel = "COALESCE ((" <> "SELECT array_to_json(array_agg(row_to_json("<>pgFmtIdent table<>"))) " <> "FROM (" <> subquery <> ") " <> pgFmtIdent table - <> "), '[]') AS " <> pgFmtIdent name + <> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) where subquery = requestToQuery schema (DbRead (Node n forst)) --the following is just to remove the warning --getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only --posible relations are Child Parent Many - getQueryParts (Node (_,(_,Nothing)) _) _ = undefined + getQueryParts (Node (_,(_,Nothing,_)) _) _ = undefined requestToQuery schema (DbMutate (Insert mainTbl (PayloadJSON (UniformObjects rows)))) = let qi = QualifiedIdentifier schema mainTbl cols = map pgFmtIdent $ fromMaybe [] (HM.keys <$> (rows V.!? 0)) @@ -463,8 +463,8 @@ pgFmtField :: QualifiedIdentifier -> Field -> SqlFragment pgFmtField table (c, jp) = pgFmtColumn table c <> pgFmtJsonPath jp pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment -pgFmtSelectItem table (f@(_, jp), Nothing) = pgFmtField table f <> pgFmtAsJsonPath jp -pgFmtSelectItem table (f@(_, jp), Just cast ) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAsJsonPath jp +pgFmtSelectItem table (f@(_, jp), Nothing, alias) = pgFmtField table f <> pgFmtAs jp alias +pgFmtSelectItem table (f@(_, jp), Just cast, alias) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs jp alias pgFmtCondition :: QualifiedIdentifier -> Filter -> SqlFragment pgFmtCondition table (Filter (col,jp) ops val) = @@ -511,9 +511,10 @@ pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs ) pgFmtJsonPath _ = "" -pgFmtAsJsonPath :: Maybe JsonPath -> SqlFragment -pgFmtAsJsonPath Nothing = "" -pgFmtAsJsonPath (Just xx) = " AS " <> last xx +pgFmtAs :: Maybe JsonPath -> Maybe Alias -> SqlFragment +pgFmtAs Nothing Nothing = "" +pgFmtAs (Just xx) Nothing = " AS " <> pgFmtIdent (last xx) +pgFmtAs _ (Just alias) = " AS " <> pgFmtIdent alias trimNullChars :: Text -> Text trimNullChars = T.takeWhile (/= '\x0') diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 2d2660516..6bd24c5bb 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -106,16 +106,17 @@ data FValue = VText Text | VForeignKey QualifiedIdentifier ForeignKey deriving ( type FieldName = Text type JsonPath = [Text] type Field = (FieldName, Maybe JsonPath) +type Alias = Text type Cast = Text type NodeName = Text -type SelectItem = (Field, Maybe Cast) +type SelectItem = (Field, Maybe Cast, Maybe Alias) type Path = [Text] data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], order::Maybe [OrderTerm] } deriving (Show, Eq) data MutateQuery = Insert { in_::TableName, qPayload::Payload } | Delete { in_::TableName, where_::[Filter] } | Update { in_::TableName, qPayload::Payload, where_::[Filter] } deriving (Show, Eq) data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq) -type ReadNode = (ReadQuery, (NodeName, Maybe Relation)) +type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias)) type ReadRequest = Tree ReadNode type MutateRequest = MutateQuery data DbRequest = DbRead ReadRequest | DbMutate MutateRequest diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 12364b4e6..4c644a3e6 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -147,16 +147,25 @@ spec = do it "dash `-` in column names is accepted" $ get "/complex_items?id=eq.3&select=id,field-with_sep" `shouldRespondWith` - [str|[{"id":3,"field-with_sep":1}]|] + [str|[{"id":3,"field-with_sep":1}]|] it "one simple column" $ get "/complex_items?select=id" `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3}] |] + it "rename simple column" $ + get "/complex_items?id=eq.1&select=myId:id" `shouldRespondWith` + [json| [{"myId":1}] |] + + it "one simple column with casting (text)" $ get "/complex_items?select=id::text" `shouldRespondWith` [json| [{"id":"1"},{"id":"2"},{"id":"3"}] |] + it "rename simple column with casting" $ + get "/complex_items?id=eq.1&select=myId:id::text" `shouldRespondWith` + [json| [{"myId":"1"}] |] + it "json column" $ get "/complex_items?id=eq.1&select=settings" `shouldRespondWith` [json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |] @@ -165,6 +174,10 @@ spec = do 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 "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" + it "fails on bad casting (data of the wrong format)" $ get "/complex_items?select=settings->foo->>bar::integer" `shouldRespondWith` ResponseMatcher { @@ -186,15 +199,28 @@ spec = do get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith` [json| [{"bar":"baz"}] |] + it "rename json subfield two levels (string)" $ + get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith` + [json| [{"myBar":"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 + 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 + it "requesting parents and children" $ get "/projects?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith` [str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] + it "requesting parents and children while renaming them" $ + get "/projects?id=eq.1&select=myId:id, name, project_client:client_id{*}, project_tasks:tasks{id, name}" `shouldRespondWith` + [str|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] + + it "requesting parents and filtering parent columns" $ get "/projects?id=eq.1&select=id, name, clients{id}" `shouldRespondWith` [str|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|] @@ -215,6 +241,10 @@ spec = do get "/tasks?select=id,users{id}" `shouldRespondWith` [str|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|] + it "requesting many<->many relation with rename" $ + get "/tasks?id=eq.1&select=id,theUsers:users{id}" `shouldRespondWith` + [str|[{"id":1,"theUsers":[{"id":1},{"id":3}]}]|] + it "requesting many<->many relation reverse" $ get "/users?select=id,tasks{id}" `shouldRespondWith`