Add PostGIS GeoJSON support
Works for GET, POST, PATCH, PUT, DELETE, RPC.
This commit is contained in:
committed by
Steve Chavez
parent
86c40e8df9
commit
870f7a39b0
@@ -29,6 +29,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #2268, Allow returning XML from single-column queries - @fjf2002
|
- #2268, Allow returning XML from single-column queries - @fjf2002
|
||||||
- #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002
|
- #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002
|
||||||
- #1959, Bulk update with PATCH - @steve-chavez
|
- #1959, Bulk update with PATCH - @steve-chavez
|
||||||
|
- #1564, Allow geojson output by specifying the `Accept: application/geo+json` media type - @steve-chavez
|
||||||
|
+ Requires postgis >= 3.0
|
||||||
|
+ Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`.
|
||||||
|
+ Resource embedding works and the embedded rows will go into the `properties` key
|
||||||
|
+ In case of multiple geometries in the same table, you can choose which one will go into the `geometry` key with the usual `?select` query parameter.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -46,12 +46,12 @@ let
|
|||||||
|
|
||||||
postgresqlVersions =
|
postgresqlVersions =
|
||||||
[
|
[
|
||||||
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14; }
|
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis ]); }
|
||||||
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13; }
|
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis ]); }
|
||||||
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12; }
|
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis ]); }
|
||||||
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11; }
|
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11.withPackages (p: [ p.postgis ]); }
|
||||||
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10; }
|
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10.withPackages (p: [ p.postgis ]); }
|
||||||
{ name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6; }
|
{ name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6.withPackages (p: [ p.postgis ]); }
|
||||||
];
|
];
|
||||||
|
|
||||||
patches =
|
patches =
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ test-suite spec
|
|||||||
Feature.Query.JsonOperatorSpec
|
Feature.Query.JsonOperatorSpec
|
||||||
Feature.Query.MultipleSchemaSpec
|
Feature.Query.MultipleSchemaSpec
|
||||||
Feature.Query.ErrorSpec
|
Feature.Query.ErrorSpec
|
||||||
|
Feature.Query.PostGISSpec
|
||||||
Feature.Query.QueryLimitedSpec
|
Feature.Query.QueryLimitedSpec
|
||||||
Feature.Query.QuerySpec
|
Feature.Query.QuerySpec
|
||||||
Feature.Query.RangeSpec
|
Feature.Query.RangeSpec
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ handleRead headersOnly identifier context@RequestContext{..} = do
|
|||||||
(shouldCount iPreferCount)
|
(shouldCount iPreferCount)
|
||||||
(iAcceptContentType == CTTextCSV)
|
(iAcceptContentType == CTTextCSV)
|
||||||
(iAcceptContentType == CTTextXML)
|
(iAcceptContentType == CTTextXML)
|
||||||
|
(iAcceptContentType == CTGeoJSON)
|
||||||
bField
|
bField
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
|
|
||||||
@@ -464,6 +465,7 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
(iAcceptContentType == CTSingularJSON)
|
(iAcceptContentType == CTSingularJSON)
|
||||||
(iAcceptContentType == CTTextCSV)
|
(iAcceptContentType == CTTextCSV)
|
||||||
(iAcceptContentType == CTTextXML)
|
(iAcceptContentType == CTTextXML)
|
||||||
|
(iAcceptContentType == CTGeoJSON)
|
||||||
(iPreferParameters == Just MultipleObjects)
|
(iPreferParameters == Just MultipleObjects)
|
||||||
bField
|
bField
|
||||||
(configDbPreparedStatements ctxConfig)
|
(configDbPreparedStatements ctxConfig)
|
||||||
@@ -551,6 +553,7 @@ writeQuery mutation identifier@QualifiedIdentifier{..} isInsert pkCols context@R
|
|||||||
(iAcceptContentType ctxApiRequest == CTSingularJSON)
|
(iAcceptContentType ctxApiRequest == CTSingularJSON)
|
||||||
isInsert
|
isInsert
|
||||||
(iAcceptContentType ctxApiRequest == CTTextCSV)
|
(iAcceptContentType ctxApiRequest == CTTextCSV)
|
||||||
|
(iAcceptContentType ctxApiRequest == CTGeoJSON)
|
||||||
(iPreferRepresentation ctxApiRequest)
|
(iPreferRepresentation ctxApiRequest)
|
||||||
pkCols
|
pkCols
|
||||||
(configDbPreparedStatements ctxConfig)
|
(configDbPreparedStatements ctxConfig)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import Protolude
|
|||||||
data ContentType
|
data ContentType
|
||||||
= CTApplicationJSON
|
= CTApplicationJSON
|
||||||
| CTSingularJSON
|
| CTSingularJSON
|
||||||
|
| CTGeoJSON
|
||||||
| CTTextCSV
|
| CTTextCSV
|
||||||
| CTTextPlain
|
| CTTextPlain
|
||||||
| CTTextXML
|
| CTTextXML
|
||||||
@@ -40,6 +41,7 @@ toHeader ct = (hContentType, toMime ct <> charset)
|
|||||||
-- | Convert from ContentType to a ByteString representing the mime type
|
-- | Convert from ContentType to a ByteString representing the mime type
|
||||||
toMime :: ContentType -> ByteString
|
toMime :: ContentType -> ByteString
|
||||||
toMime CTApplicationJSON = "application/json"
|
toMime CTApplicationJSON = "application/json"
|
||||||
|
toMime CTGeoJSON = "application/geo+json"
|
||||||
toMime CTTextCSV = "text/csv"
|
toMime CTTextCSV = "text/csv"
|
||||||
toMime CTTextPlain = "text/plain"
|
toMime CTTextPlain = "text/plain"
|
||||||
toMime CTTextXML = "text/xml"
|
toMime CTTextXML = "text/xml"
|
||||||
@@ -55,6 +57,7 @@ decodeContentType :: BS.ByteString -> ContentType
|
|||||||
decodeContentType ct =
|
decodeContentType ct =
|
||||||
case BS.takeWhile (/= BS.c2w ';') ct of
|
case BS.takeWhile (/= BS.c2w ';') ct of
|
||||||
"application/json" -> CTApplicationJSON
|
"application/json" -> CTApplicationJSON
|
||||||
|
"application/geo+json" -> CTGeoJSON
|
||||||
"text/csv" -> CTTextCSV
|
"text/csv" -> CTTextCSV
|
||||||
"text/plain" -> CTTextPlain
|
"text/plain" -> CTTextPlain
|
||||||
"text/xml" -> CTTextXML
|
"text/xml" -> CTTextXML
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ module PostgREST.Query.SqlFragment
|
|||||||
, SqlFragment
|
, SqlFragment
|
||||||
, asBinaryF
|
, asBinaryF
|
||||||
, asCsvF
|
, asCsvF
|
||||||
|
, asGeoJsonF
|
||||||
, asJsonF
|
, asJsonF
|
||||||
, asJsonSingleF
|
, asJsonSingleF
|
||||||
, asXmlF
|
, asXmlF
|
||||||
@@ -182,6 +183,9 @@ asJsonSingleF returnsScalar
|
|||||||
asXmlF :: FieldName -> SqlFragment
|
asXmlF :: FieldName -> SqlFragment
|
||||||
asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"
|
asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"
|
||||||
|
|
||||||
|
asGeoJsonF :: SqlFragment
|
||||||
|
asGeoJsonF = "json_build_object('type', 'FeatureCollection', 'features', coalesce(json_agg(ST_AsGeoJSON(_postgrest_t)::json), '[]'))"
|
||||||
|
|
||||||
asBinaryF :: FieldName -> SqlFragment
|
asBinaryF :: FieldName -> SqlFragment
|
||||||
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
|
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
|
||||||
|
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ import Protolude
|
|||||||
-}
|
-}
|
||||||
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Error [GucHeader], Either Error (Maybe Status))
|
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Error [GucHeader], Either Error (Maybe Status))
|
||||||
|
|
||||||
createWriteStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool ->
|
createWriteStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool ->
|
||||||
PreferRepresentation -> [Text] -> Bool ->
|
PreferRepresentation -> [Text] -> Bool ->
|
||||||
SQL.Statement () ResultsWithCount
|
SQL.Statement () ResultsWithCount
|
||||||
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys =
|
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv asGeoJson rep pKeys =
|
||||||
SQL.dynamicallyParameterized snippet decodeStandard
|
SQL.dynamicallyParameterized snippet decodeStandard
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
@@ -74,6 +74,7 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
|
|||||||
bodyF
|
bodyF
|
||||||
| rep /= Full = "''"
|
| rep /= Full = "''"
|
||||||
| asCsv = asCsvF
|
| asCsv = asCsvF
|
||||||
|
| asGeoJson = asGeoJsonF
|
||||||
| wantSingle = asJsonSingleF False
|
| wantSingle = asJsonSingleF False
|
||||||
| otherwise = asJsonF False
|
| otherwise = asJsonF False
|
||||||
|
|
||||||
@@ -86,9 +87,9 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
|
|||||||
decodeStandard =
|
decodeStandard =
|
||||||
fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow
|
fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow
|
||||||
|
|
||||||
createReadStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
|
createReadStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
|
||||||
SQL.Statement () ResultsWithCount
|
SQL.Statement () ResultsWithCount
|
||||||
createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml binaryField =
|
createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml asGeoJson binaryField =
|
||||||
SQL.dynamicallyParameterized snippet decodeStandard
|
SQL.dynamicallyParameterized snippet decodeStandard
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
@@ -109,6 +110,7 @@ createReadStatement selectQuery countQuery isSingle countTotal asCsv asXml binar
|
|||||||
bodyF
|
bodyF
|
||||||
| asCsv = asCsvF
|
| asCsv = asCsvF
|
||||||
| isSingle = asJsonSingleF False
|
| isSingle = asJsonSingleF False
|
||||||
|
| asGeoJson = asGeoJsonF
|
||||||
| isJust binaryField && asXml = asXmlF $ fromJust binaryField
|
| isJust binaryField && asXml = asXmlF $ fromJust binaryField
|
||||||
| isJust binaryField = asBinaryF $ fromJust binaryField
|
| isJust binaryField = asBinaryF $ fromJust binaryField
|
||||||
| otherwise = asJsonF False
|
| otherwise = asJsonF False
|
||||||
@@ -130,9 +132,9 @@ standardRow = (,,,,,) <$> nullableColumn HD.int8 <*> column HD.int8
|
|||||||
type ProcResults = (Maybe Int64, Int64, ByteString, Either Error [GucHeader], Either Error (Maybe Status))
|
type ProcResults = (Maybe Int64, Int64, ByteString, Either Error [GucHeader], Either Error (Maybe Status))
|
||||||
|
|
||||||
callProcStatement :: Bool -> Bool -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
callProcStatement :: Bool -> Bool -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
||||||
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
|
Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
|
||||||
SQL.Statement () ProcResults
|
SQL.Statement () ProcResults
|
||||||
callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal asSingle asCsv asXml multObjects binaryField =
|
callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal asSingle asCsv asXml asGeoJson multObjects binaryField =
|
||||||
SQL.dynamicallyParameterized snippet decodeProc
|
SQL.dynamicallyParameterized snippet decodeProc
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
@@ -152,6 +154,7 @@ callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQue
|
|||||||
bodyF
|
bodyF
|
||||||
| asSingle = asJsonSingleF returnsScalar
|
| asSingle = asJsonSingleF returnsScalar
|
||||||
| asCsv = asCsvF
|
| asCsv = asCsvF
|
||||||
|
| asGeoJson = asGeoJsonF
|
||||||
| isJust binaryField && asXml = asXmlF $ fromJust binaryField
|
| isJust binaryField && asXml = asXmlF $ fromJust binaryField
|
||||||
| isJust binaryField = asBinaryF $ fromJust binaryField
|
| isJust binaryField = asBinaryF $ fromJust binaryField
|
||||||
| returnsSingle
|
| returnsSingle
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ requestContentTypes conf action path =
|
|||||||
++ rawContentTypes conf
|
++ rawContentTypes conf
|
||||||
++ [CTOpenAPI | pIsRootSpec path]
|
++ [CTOpenAPI | pIsRootSpec path]
|
||||||
defaultContentTypes =
|
defaultContentTypes =
|
||||||
[CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
[CTApplicationJSON, CTSingularJSON, CTGeoJSON, CTTextCSV]
|
||||||
|
|
||||||
rawContentTypes :: AppConfig -> [ContentType]
|
rawContentTypes :: AppConfig -> [ContentType]
|
||||||
rawContentTypes AppConfig{..} =
|
rawContentTypes AppConfig{..} =
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
module Feature.Query.PostGISSpec where
|
||||||
|
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import Network.HTTP.Types
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100)
|
||||||
|
|
||||||
|
import Protolude hiding (get)
|
||||||
|
import SpecHelper
|
||||||
|
|
||||||
|
spec :: PgVersion -> SpecWith ((), Application)
|
||||||
|
spec actualPgVersion = describe "PostGIS features" $
|
||||||
|
context "GeoJSON output" $ do
|
||||||
|
it "works for a table that has a geometry column" $
|
||||||
|
request methodGet "/shops"
|
||||||
|
[("Accept", "application/geo+json")] "" `shouldRespondWith`
|
||||||
|
[json| {
|
||||||
|
"type" : "FeatureCollection",
|
||||||
|
"features" : [
|
||||||
|
{"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.10044,42.373695]}, "properties": {"id": 1, "address": "1369 Cambridge St"}}
|
||||||
|
, {"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.10543,42.366432]}, "properties": {"id": 2, "address": "757 Massachusetts Ave"}}
|
||||||
|
, {"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.081924,42.36437]}, "properties": {"id": 3, "address": "605 W Kendall St"}}
|
||||||
|
]} |]
|
||||||
|
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
|
||||||
|
|
||||||
|
it "fails for a table that doesn't have a geometry column" $
|
||||||
|
request methodGet "/projects"
|
||||||
|
[("Accept", "application/geo+json")] "" `shouldRespondWith`
|
||||||
|
[json| {"hint":null,"details":null,"code":"22023","message":"geometry column is missing"} |]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "gives an empty features array on no rows" $
|
||||||
|
request methodGet "/shops?id=gt.3"
|
||||||
|
[("Accept", "application/geo+json")] "" `shouldRespondWith`
|
||||||
|
[json| {
|
||||||
|
"type" : "FeatureCollection",
|
||||||
|
"features" : []} |]
|
||||||
|
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
|
||||||
|
|
||||||
|
it "must include the geometry column when using ?select" $
|
||||||
|
request methodGet "/shops?select=id,shop_geom&id=eq.1"
|
||||||
|
[("Accept", "application/geo+json")] "" `shouldRespondWith`
|
||||||
|
[json| {
|
||||||
|
"type" : "FeatureCollection",
|
||||||
|
"features" : [
|
||||||
|
{"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.10044,42.373695]}, "properties": {"id": 1}}
|
||||||
|
] }|]
|
||||||
|
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
|
||||||
|
|
||||||
|
it "works with resource embedding" $
|
||||||
|
request methodGet "/shops?select=*,shop_bles(*)&id=eq.1"
|
||||||
|
[("Accept", "application/geo+json")] "" `shouldRespondWith`
|
||||||
|
(if actualPgVersion >= pgVersion100
|
||||||
|
then [json| {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" },
|
||||||
|
"properties": {
|
||||||
|
"address": "1369 Cambridge St", "id": 1,
|
||||||
|
"shop_bles": [
|
||||||
|
{ "id": 1, "name": "Beacon-1", "shop_id": 1 ,
|
||||||
|
"coords": { "coordinates": [ -71.10044, 42.373695 ], "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Point" },
|
||||||
|
"range_area": {
|
||||||
|
"coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ],
|
||||||
|
"crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Polygon" }
|
||||||
|
},
|
||||||
|
{ "coords": { "coordinates": [ -71.10044, 42.373695 ], "crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Point" },
|
||||||
|
"id": 2, "name": "Beacon-2", "shop_id": 1,
|
||||||
|
"range_area": {
|
||||||
|
"coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ],
|
||||||
|
"crs": { "properties": { "name": "EPSG:4326" }, "type": "name" }, "type": "Polygon" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}|]
|
||||||
|
else [json| {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" },
|
||||||
|
"properties": {
|
||||||
|
"address": "1369 Cambridge St", "id": 1,
|
||||||
|
"shop_bles": [
|
||||||
|
{ "id": 1, "name": "Beacon-1", "shop_id": 1 ,
|
||||||
|
"coords": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" },
|
||||||
|
"range_area": {
|
||||||
|
"coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ],
|
||||||
|
"type": "Polygon" }
|
||||||
|
},
|
||||||
|
{ "coords": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" },
|
||||||
|
"id": 2, "name": "Beacon-2", "shop_id": 1,
|
||||||
|
"range_area": {
|
||||||
|
"coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ],
|
||||||
|
"type": "Polygon" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}|])
|
||||||
|
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
|
||||||
|
|
||||||
|
it "works with RPC" $
|
||||||
|
request methodGet "/rpc/get_shop?id=1"
|
||||||
|
[("Accept", "application/geo+json")] "" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"type" : "FeatureCollection",
|
||||||
|
"features" : [
|
||||||
|
{"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.10044,42.373695]}, "properties": {"id": 1, "address": "1369 Cambridge St"} }
|
||||||
|
]
|
||||||
|
}|]
|
||||||
|
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
|
||||||
|
|
||||||
|
it "works with Prefer: return=representation after POST" $
|
||||||
|
request methodPost "/shops"
|
||||||
|
[("Accept", "application/geo+json"), ("Prefer", "return=representation")] [json|
|
||||||
|
{"id": 4, "address": "1354 Massachusetts Ave", "shop_geom": "SRID=4326;POINT(-71.11834 42.373238)"}
|
||||||
|
|] `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": { "coordinates": [ -71.11834, 42.373238 ], "type": "Point" },
|
||||||
|
"properties": { "address": "1354 Massachusetts Ave", "id": 4 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with Prefer: return=representation after PATCH" $
|
||||||
|
request methodPatch "/shops?id=eq.3"
|
||||||
|
[("Accept", "application/geo+json"), ("Prefer", "return=representation")]
|
||||||
|
[json| { "address": "1354 Massachusetts Avenue"} |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": { "coordinates": [-71.081924,42.36437], "type": "Point" },
|
||||||
|
"properties": { "address": "1354 Massachusetts Avenue", "id": 3 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with Prefer: return=representation after PUT" $
|
||||||
|
request methodPut "/shops?id=eq.3"
|
||||||
|
[("Accept", "application/geo+json"), ("Prefer", "return=representation")]
|
||||||
|
[json| { "id": 3, "address": "1354 Massachusetts Avenue"} |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": { "coordinates": [-71.081924,42.36437], "type": "Point" },
|
||||||
|
"properties": { "address": "1354 Massachusetts Avenue", "id": 3 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with Prefer: return=representation after DELETE" $
|
||||||
|
request methodDelete "/shops?id=eq.3"
|
||||||
|
[("Accept", "application/geo+json"), ("Prefer", "return=representation")] "" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"type" : "FeatureCollection",
|
||||||
|
"features" : [
|
||||||
|
{"type": "Feature", "geometry": {"type":"Point","coordinates":[-71.081924,42.36437]}, "properties": {"id": 3, "address": "605 W Kendall St"}}
|
||||||
|
]
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
|
||||||
|
}
|
||||||
|
|
||||||
|
context "multiple geometry columns" $
|
||||||
|
it "can select the geometry column to get as a feature with ?select" $ do
|
||||||
|
request methodGet "/shop_bles?select=id,name,coords&id=eq.1"
|
||||||
|
[("Accept", "application/geo+json")] "" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"type" : "FeatureCollection",
|
||||||
|
"features" : [
|
||||||
|
{"type": "Feature",
|
||||||
|
"geometry": {"type":"Point","coordinates":[-71.10044,42.373695]},
|
||||||
|
"properties": {"id": 1, "name": "Beacon-1"}
|
||||||
|
}
|
||||||
|
]}|]
|
||||||
|
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
|
||||||
|
|
||||||
|
request methodGet "/shop_bles?select=id,name,range_area&id=eq.1"
|
||||||
|
[("Accept", "application/geo+json")] "" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"type" : "FeatureCollection",
|
||||||
|
"features" : [
|
||||||
|
{"type": "Feature",
|
||||||
|
"geometry": {"type":"Polygon","coordinates":[[[-71.100452542,42.373870833],[-71.100480705,42.373771262],[-71.100396886,42.373758382],[-71.100370064,42.373858449],[-71.100452542,42.373870833]]]},
|
||||||
|
"properties": {"id": 1, "name": "Beacon-1"}}
|
||||||
|
]}|]
|
||||||
|
{ matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"] }
|
||||||
|
|
||||||
|
it "gets the geojson geometry object with the regular application/json output" $
|
||||||
|
request methodGet "/shops?id=eq.1" [] "" `shouldRespondWith`
|
||||||
|
(if actualPgVersion >= pgVersion100
|
||||||
|
then [json|[{
|
||||||
|
"id":1,"address":"1369 Cambridge St",
|
||||||
|
"shop_geom":{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-71.10044,42.373695]}
|
||||||
|
}]|]
|
||||||
|
else [json|[{
|
||||||
|
"address": "1369 Cambridge St",
|
||||||
|
"id": 1,
|
||||||
|
"shop_geom": { "coordinates": [ -71.10044, 42.373695 ], "type": "Point" }
|
||||||
|
}]|])
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
+3
-1
@@ -44,6 +44,7 @@ import qualified Feature.Query.HtmlRawOutputSpec
|
|||||||
import qualified Feature.Query.InsertSpec
|
import qualified Feature.Query.InsertSpec
|
||||||
import qualified Feature.Query.JsonOperatorSpec
|
import qualified Feature.Query.JsonOperatorSpec
|
||||||
import qualified Feature.Query.MultipleSchemaSpec
|
import qualified Feature.Query.MultipleSchemaSpec
|
||||||
|
import qualified Feature.Query.PostGISSpec
|
||||||
import qualified Feature.Query.QueryLimitedSpec
|
import qualified Feature.Query.QueryLimitedSpec
|
||||||
import qualified Feature.Query.QuerySpec
|
import qualified Feature.Query.QuerySpec
|
||||||
import qualified Feature.Query.RangeSpec
|
import qualified Feature.Query.RangeSpec
|
||||||
@@ -199,8 +200,9 @@ main = do
|
|||||||
describe "Feature.Query.ErrorSpec" Feature.Query.ErrorSpec.spec
|
describe "Feature.Query.ErrorSpec" Feature.Query.ErrorSpec.spec
|
||||||
|
|
||||||
-- this test runs with an extra search path
|
-- this test runs with an extra search path
|
||||||
parallel $ before extraSearchPathApp $
|
parallel $ before extraSearchPathApp $ do
|
||||||
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
|
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
|
||||||
|
describe "Feature.Query.PostGISSpec" $ Feature.Query.PostGISSpec.spec actualPgVersion
|
||||||
|
|
||||||
-- this test runs with a root spec function override
|
-- this test runs with a root spec function override
|
||||||
parallel $ before rootSpecApp $
|
parallel $ before rootSpecApp $
|
||||||
|
|||||||
Vendored
+11
@@ -784,3 +784,14 @@ INSERT INTO test.bulk_update_items (id, name, observation) VALUES (1, 'item-1',
|
|||||||
|
|
||||||
TRUNCATE TABLE test.bulk_update_items_cpk CASCADE;
|
TRUNCATE TABLE test.bulk_update_items_cpk CASCADE;
|
||||||
INSERT INTO test.bulk_update_items_cpk (id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
|
INSERT INTO test.bulk_update_items_cpk (id, name, observation) VALUES (1, 'item-1', NULL), (2, 'item-2', NULL), (3, 'item-3', NULL);
|
||||||
|
|
||||||
|
TRUNCATE TABLE shops CASCADE;
|
||||||
|
INSERT INTO shops(id, address, shop_geom) VALUES(1, '1369 Cambridge St', 'SRID=4326;POINT(-71.10044 42.373695)');
|
||||||
|
INSERT INTO shops(id, address, shop_geom) VALUES(2, '757 Massachusetts Ave', 'SRID=4326;POINT(-71.10543 42.366432)');
|
||||||
|
INSERT INTO shops(id, address, shop_geom) VALUES(3, '605 W Kendall St', 'SRID=4326;POINT(-71.081924 42.36437)');
|
||||||
|
|
||||||
|
TRUNCATE TABLE shop_bles CASCADE;
|
||||||
|
INSERT INTO shop_bles(id, name, coords, shop_id, range_area) VALUES(1, 'Beacon-1', 'SRID=4326;POINT(-71.10044 42.373695)', 1,
|
||||||
|
extensions.ST_GeomFromGeoJSON('{"type": "Polygon", "coordinates": [ [ [ -71.10045254230499, 42.37387083326593 ], [ -71.10048070549963, 42.37377126199953 ], [ -71.10039688646793, 42.37375838212269 ], [ -71.10037006437777, 42.37385844878863 ], [ -71.10045254230499, 42.37387083326593 ] ] ]}'));
|
||||||
|
INSERT INTO shop_bles(id, name, coords, shop_id, range_area) VALUES(2, 'Beacon-2', 'SRID=4326;POINT(-71.10044 42.373695)', 1,
|
||||||
|
extensions.ST_GeomFromGeoJSON('{"type": "Polygon", "coordinates": [ [ [ -71.10034391283989, 42.37385299961788 ], [ -71.10036939382553, 42.373756895982865 ], [ -71.1002916097641, 42.373745997623224 ], [ -71.1002641171217, 42.37384408279195 ], [ -71.10034391283989, 42.37385299961788 ] ] ]}'));
|
||||||
|
|||||||
Vendored
+2
@@ -188,6 +188,8 @@ GRANT ALL ON TABLE
|
|||||||
, view_test
|
, view_test
|
||||||
, bulk_update_items
|
, bulk_update_items
|
||||||
, bulk_update_items_cpk
|
, bulk_update_items_cpk
|
||||||
|
, shops
|
||||||
|
, shop_bles
|
||||||
TO postgrest_test_anonymous;
|
TO postgrest_test_anonymous;
|
||||||
|
|
||||||
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
||||||
|
|||||||
Vendored
+20
@@ -2649,3 +2649,23 @@ CREATE TABLE test.bulk_update_items_cpk (
|
|||||||
observation TEXT,
|
observation TEXT,
|
||||||
PRIMARY KEY (id, name)
|
PRIMARY KEY (id, name)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create extension if not exists postgis with schema extensions;
|
||||||
|
|
||||||
|
create table shops (
|
||||||
|
id int primary key
|
||||||
|
, address text
|
||||||
|
, shop_geom extensions.geometry(POINT, 4326)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table shop_bles (
|
||||||
|
id int primary key
|
||||||
|
, name text
|
||||||
|
, coords extensions.geometry(POINT, 4326)
|
||||||
|
, range_area extensions.geometry(POLYGON, 4326)
|
||||||
|
, shop_id int references shops(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create function get_shop(id int) returns shops as $$
|
||||||
|
select * from shops where id = $1;
|
||||||
|
$$ language sql;
|
||||||
|
|||||||
Reference in New Issue
Block a user