Add PostGIS GeoJSON support

Works for GET, POST, PATCH, PUT, DELETE, RPC.
This commit is contained in:
steve-chavez
2022-06-22 22:16:44 -05:00
committed by Steve Chavez
parent 86c40e8df9
commit 870f7a39b0
13 changed files with 300 additions and 14 deletions
+232
View File
@@ -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
View File
@@ -44,6 +44,7 @@ import qualified Feature.Query.HtmlRawOutputSpec
import qualified Feature.Query.InsertSpec
import qualified Feature.Query.JsonOperatorSpec
import qualified Feature.Query.MultipleSchemaSpec
import qualified Feature.Query.PostGISSpec
import qualified Feature.Query.QueryLimitedSpec
import qualified Feature.Query.QuerySpec
import qualified Feature.Query.RangeSpec
@@ -199,8 +200,9 @@ main = do
describe "Feature.Query.ErrorSpec" Feature.Query.ErrorSpec.spec
-- this test runs with an extra search path
parallel $ before extraSearchPathApp $
parallel $ before extraSearchPathApp $ do
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
describe "Feature.Query.PostGISSpec" $ Feature.Query.PostGISSpec.spec actualPgVersion
-- this test runs with a root spec function override
parallel $ before rootSpecApp $
+11
View File
@@ -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;
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 ] ] ]}'));
+2
View File
@@ -188,6 +188,8 @@ GRANT ALL ON TABLE
, view_test
, bulk_update_items
, bulk_update_items_cpk
, shops
, shop_bles
TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
+20
View File
@@ -2649,3 +2649,23 @@ CREATE TABLE test.bulk_update_items_cpk (
observation TEXT,
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;