Add Preference-Applied header for POST upsert

- Ensure creating nothing on ignore-duplicates succeeds
- Refactor locationF query
This commit is contained in:
steve-chavez
2018-02-21 07:33:54 -05:00
committed by Steve Chávez
parent 6675821c64
commit 108f3cd651
5 changed files with 42 additions and 25 deletions
+2 -2
View File
@@ -116,8 +116,8 @@ userApiRequest schema req reqBody
, iPreferRepresentation = representation , iPreferRepresentation = representation
, iPreferSingleObjectParameter = singleObject , iPreferSingleObjectParameter = singleObject
, iPreferCount = hasPrefer "count=exact" , iPreferCount = hasPrefer "count=exact"
, iPreferResolution = if hasPrefer "resolution=merge-duplicates" then Just MergeDuplicates , iPreferResolution = if hasPrefer (show MergeDuplicates) then Just MergeDuplicates
else if hasPrefer "resolution=ignore-duplicates" then Just IgnoreDuplicates else if hasPrefer (show IgnoreDuplicates) then Just IgnoreDuplicates
else Nothing else Nothing
, iFilters = filters , iFilters = filters
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ] , iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
+6 -3
View File
@@ -157,10 +157,10 @@ app dbStructure proc conf apiRequest =
&& iPreferRepresentation apiRequest == Full && iPreferRepresentation apiRequest == Full
then return $ singularityError (toInteger nRows) then return $ singularityError (toInteger nRows)
else do else do
let stm = createWriteStatement sq mq let pkCols = tablePKCols dbStructure tSchema tName
stm = createWriteStatement sq mq
(contentType == CTSingularJSON) isSingle (contentType == CTSingularJSON) isSingle
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) (contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols
(tablePKCols dbStructure tSchema tName)
row <- H.query (toS pjRaw) stm row <- H.query (toS pjRaw) stm
let (_, _, fs, body) = extractQueryResult row let (_, _, fs, body) = extractQueryResult row
headers = catMaybes [ headers = catMaybes [
@@ -172,6 +172,9 @@ app dbStructure proc conf apiRequest =
else Nothing else Nothing
, Just . contentRangeH 1 0 $ , Just . contentRangeH 1 0 $
toInteger <$> if shouldCount then Just nRows else Nothing toInteger <$> if shouldCount then Just nRows else Nothing
, if null pkCols
then Nothing
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
] ]
return . responseLBS status201 headers $ return . responseLBS status201 headers $
+8 -12
View File
@@ -121,7 +121,7 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys
"'' AS total_result_set", -- when updateing it does not make sense "'' AS total_result_set", -- when updateing it does not make sense
"pg_catalog.count(_postgrest_t) AS page_total", "pg_catalog.count(_postgrest_t) AS page_total",
if wantHdrs if wantHdrs
then locationF pKeys then "coalesce(" <> locationF pKeys <> ", " <> noLocationF <> ")"
else noLocationF <> " AS header", else noLocationF <> " AS header",
if rep == Full if rep == Full
then bodyF <> " AS body" then bodyF <> " AS body"
@@ -278,7 +278,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest
requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType pKeys) onConflct logicForest returnings)) = requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType pKeys) onConflct logicForest returnings)) =
unwords [ unwords [
("WITH " <> ignoredBody) `emptyOnFalse` not payloadIsEmpty, ("WITH " <> ignoredBody) `emptyOnFalse` not payloadIsEmpty,
"INSERT INTO ", fromQi qi, if payloadIsEmpty then " " else "(" <> cols <> ") ", "INSERT INTO ", fromQi qi, if payloadIsEmpty then " " else "(" <> cols <> ")",
case (pType, payloadIsEmpty) of case (pType, payloadIsEmpty) of
(PJArray _, True) -> "SELECT null WHERE false" (PJArray _, True) -> "SELECT null WHERE false"
(PJObject, True) -> "DEFAULT VALUES" (PJObject, True) -> "DEFAULT VALUES"
@@ -369,16 +369,12 @@ asBinaryF :: FieldName -> SqlFragment
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')" asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
locationF :: [Text] -> SqlFragment locationF :: [Text] -> SqlFragment
locationF pKeys = locationF pKeys = [qc|(
"(" <> WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1)
" WITH s AS (SELECT row_to_json(ss) as r from " <> sourceCTEName <> " as ss limit 1)" <> SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))
" SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))" <> FROM data CROSS JOIN json_each_text(data.row) AS json_data
" FROM s, json_each_text(s.r) AS json_data" <> {("WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')") `emptyOnFalse` null pKeys}
( )|]
if null pKeys
then ""
else " WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')"
) <> ")"
limitF :: NonnegRange -> SqlFragment limitF :: NonnegRange -> SqlFragment
limitF r = if r == allRange limitF r = if r == allRange
+4 -1
View File
@@ -26,7 +26,10 @@ data ApiRequestError = ActionInappropriate
| InvalidFilters | InvalidFilters
deriving (Show, Eq) deriving (Show, Eq)
data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving (Eq, Show) data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq
instance Show PreferResolution where
show MergeDuplicates = "resolution=merge-duplicates"
show IgnoreDuplicates = "resolution=ignore-duplicates"
data DbStructure = DbStructure { data DbStructure = DbStructure {
dbTables :: [Table] dbTables :: [Table]
+22 -7
View File
@@ -28,7 +28,7 @@ spec =
{ "name": "C", "rank": 1 } { "name": "C", "rank": 1 }
]|] ]|]
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [matchContentTypeJson] , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
} }
it "INSERTs and UPDATEs row on composite pk conflict" $ it "INSERTs and UPDATEs row on composite pk conflict" $
@@ -41,7 +41,7 @@ spec =
{ "first_name": "Peter S.", "last_name": "Yang", "salary": "$42,000.00", "company": null, "occupation": null } { "first_name": "Peter S.", "last_name": "Yang", "salary": "$42,000.00", "company": null, "occupation": null }
]|] ]|]
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [matchContentTypeJson] , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
} }
context "when Prefer: resolution=ignore-duplicates is specified" $ do context "when Prefer: resolution=ignore-duplicates is specified" $ do
@@ -54,7 +54,7 @@ spec =
{ "name": "PHP", "rank": 9 } { "name": "PHP", "rank": 9 }
]|] ]|]
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [matchContentTypeJson] , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
} }
it "INSERTs and ignores rows on composite pk conflict" $ it "INSERTs and ignores rows on composite pk conflict" $
@@ -66,25 +66,40 @@ spec =
{ "first_name": "Sara M.", "last_name": "Torpey", "salary": "$60,000.00", "company": "Burstein-Applebee", "occupation": "Soil scientist" } { "first_name": "Sara M.", "last_name": "Torpey", "salary": "$60,000.00", "company": "Burstein-Applebee", "occupation": "Soil scientist" }
]|] ]|]
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [matchContentTypeJson] , matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
} }
it "succeeds if the table has only PK cols and no other cols" $ do it "succeeds if the table has only PK cols and no other cols" $ do
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|] [json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]
`shouldRespondWith` `shouldRespondWith`
[json|[ { "id": 3} ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } [json|[ { "id": 3} ]|]
{ matchStatus = 201 ,
matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates",
matchContentTypeJson] }
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|] [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]
`shouldRespondWith` `shouldRespondWith`
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } [json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]
{ matchStatus = 201 ,
matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates",
matchContentTypeJson] }
it "succeeds and ignores the Prefer: resolution header if the table has no PK" $ it "succeeds and ignores the Prefer: resolution header(no Preference-Applied present) if the table has no PK" $
request methodPost "/no_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] request methodPost "/no_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json|[ { "a": "1", "b": "0" } ]|] [json|[ { "a": "1", "b": "0" } ]|]
`shouldRespondWith` `shouldRespondWith`
[json|[ { "a": "1", "b": "0" } ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] } [json|[ { "a": "1", "b": "0" } ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
it "succeeds if not a single resource is created" $ do
request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[ { "name": "Java", "rank": 1 } ]|] `shouldRespondWith`
[json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[ { "name": "Java", "rank": 1 }, { "name": "C", "rank": 2 } ]|] `shouldRespondWith`
[json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
context "with PUT" $ do context "with PUT" $ do
context "Restrictions" $ do context "Restrictions" $ do
it "fails if Range is specified" $ it "fails if Range is specified" $