Querying PostGIS columns with Supabase and TypeScript

Carto, OpenStreetMap contributors

Supabase's type generator is very helpful when using supabase-js with TypeScript. But when your query selects a PostGIS geometry type column, its generated type will be unknown. We have a few options:

Type overrides

supabase-js queries Supabase's PostgREST API, which actually returns geometry columns in a usable form (column name in this example is location):

{
  "location": {
    "type": "Point",
    "crs":{
      "type": "name",
      "properties": {
        "name": "EPSG:4326"
      }
    },
    "coordinates": [103.80138721340607, 1.449137453350455]
  }
}
 

So we can just override the generated types in the client, similar to how it's done in the official docs. E.g. assuming you know the value is a Point and you're just interested in the location:

import type { MergeDeep } from 'type-fest'
import type { Database as DatabaseGenerated } from './database.types'

export type Database = MergeDeep<DatabaseGenerated, {
    public: {
        Tables: {
            your_table: {
                Row: {
                    location: {
                        type: "Point",
                        coordinates: number[],
                    },
                },
            },
        },
    },
}>;

However the response including the unnecessary type and crs fields is a bit long, which might make a difference if your query is returning thousands of points.

A view to unpack the geometry

Postgres views are amazingly useful to simplify your queries, even more so with Supabase and PostgREST. We can create a view using PostGIS functions to unpack the geometry into built-in types. E.g. in this simple example using a Point, we can use ST_X and ST_Y to get its coordinate components:

create view your_table_with_coordinates
with
  (security_invoker = true) as
select
  *,
  st_x (location) as location_lon,
  st_y (location) as location_lat
from
  your_table;

grant all on table your_table_with_coordinates to authenticated, anon;

When using views, always consider using security_invoker = true so that your view applies the same row level security rules as the underlying table (PostgreSQL's default is that the view will use the creator's permissions!). Also review to which roles to grant permissions.

Returning coordinates from a function

If you need to do some more transformations or filtering anyway, instead of a view, you can also use a function to return the unpacked coordinates. Those are callable via PostgREST as well. Note how we also create a custom object type to allow providing latitude and longitude as an argument.

do $$
  begin
    create type coordinates as (lon numeric, lat numeric);
    exception when duplicate_object then null;
  end
$$;

create or replace function get_points (
  within_1km_radius_from coordinates default null
) returns table (
  location_lon float,
  location_lat float
)
language plpgsql security invoker
set search_path to '' as $$
  begin
    return query
    select
      extensions.st_x(location::extensions.geometry) as lon,
      extensions.st_y(location::extensions.geometry) as lat
    from public.points p
    where
      case when within_1km_radius_from is not null then
        extensions.st_dwithin(p.location::extensions.geography, extensions.st_geomfromtext('POINT(' || within_1km_radius_from.lon || ' ' || within_1km_radius_from.lat || ')', 4326)::extensions.geography, '1000')
      else
        true 
      end;
  end;
$$;