Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/lib/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PostgrestBuilder, PostgrestMaybeSingleResponse, PostgrestSingleResponse } from './types'
import { PostgrestBuilder, PostgrestMaybeSingleResponse, PostgrestResponse, PostgrestSingleResponse } from './types'

/**
* Post-filters (transforms)
Expand Down Expand Up @@ -120,4 +120,51 @@ export default class PostgrestTransformBuilder<T> extends PostgrestBuilder<T> {
this.headers['Accept'] = 'text/csv'
return this as PromiseLike<PostgrestSingleResponse<string>>
}

/**
* Set the response type to GeoJSON.
*/
geojson(): PromiseLike<PostgrestSingleResponse<Record<string, unknown>>> {
this.headers['Accept'] = 'application/geo+json'
return this as PromiseLike<PostgrestSingleResponse<Record<string, unknown>>>
}

/**
* Obtains the EXPLAIN plan for this request.
*
* @param analyze If `true`, the query will be executed and the actual run time will be displayed.
* @param verbose If `true`, the query identifier will be displayed and the result will include the output columns of the query.
* @param settings If `true`, include information on configuration parameters that affect query planning.
* @param buffers If `true`, include information on buffer usage.
* @param wal If `true`, include information on WAL record generation
*/
explain({
analyze = false,
verbose = false,
settings = false,
buffers = false,
wal = false,
}: {
analyze?: boolean
verbose?: boolean
settings?: boolean
buffers?: boolean
wal?: boolean
} = {}): PromiseLike<PostgrestResponse<Record<string, unknown>>> {
const options = [
analyze ? 'analyze' : null,
verbose ? 'verbose' : null,
settings ? 'settings' : null,
buffers ? 'buffers' : null,
wal ? 'wal' : null,
]
.filter(Boolean)
.join('|')
// An Accept header can carry multiple media types but postgrest-js always sends one
const forMediatype = this.headers['Accept']
this.headers[
'Accept'
] = `application/vnd.pgrst.plan+json; for="${forMediatype}"; options=${options};`
return this as PromiseLike<PostgrestResponse<Record<string, unknown>>>
}
}
6 changes: 4 additions & 2 deletions test/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1988,8 +1988,8 @@ Object {
"count": null,
"data": null,
"error": null,
"status": 200,
"statusText": "OK",
"status": 204,
"statusText": "No Content",
}
`;

Expand Down Expand Up @@ -2305,6 +2305,8 @@ Object {

exports[`throwOnError setting at the client level - rpc 1`] = `
Object {
"code": "PGRST202",
"details": null,
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the public.missing_fn() function or the public.missing_fn function with a single unnamed json or jsonb parameter in the schema cache",
}
Expand Down
12 changes: 10 additions & 2 deletions test/db/00-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ CREATE TABLE public.users (
status user_status DEFAULT 'ONLINE'::public.user_status,
catchphrase tsvector DEFAULT null
);
ALTER TABLE public.users REPLICA IDENTITY FULL; -- Send "previous data" to supabase
ALTER TABLE public.users REPLICA IDENTITY FULL; -- Send "previous data" to supabase
COMMENT ON COLUMN public.users.data IS 'For unstructured data and prototyping.';

-- CHANNELS
Expand Down Expand Up @@ -47,10 +47,18 @@ RETURNS TABLE(username text, status user_status) AS $$
SELECT username, status from users WHERE username=name_param;
$$ LANGUAGE SQL IMMUTABLE;

CREATE FUNCTION public.void_func()
CREATE FUNCTION public.void_func()
RETURNS void AS $$
$$ LANGUAGE SQL;

create extension postgis;

create table public.shops (
id int primary key
, address text
, shop_geom geometry(POINT, 4326)
);

-- SECOND SCHEMA USERS
CREATE TYPE personal.user_status AS ENUM ('ONLINE', 'OFFLINE');
CREATE TABLE personal.users(
Expand Down
6 changes: 5 additions & 1 deletion test/db/01-dummy-data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ VALUES
('kiwicopple', 'OFFLINE', '[25,35)'::int4range),
('awailas', 'ONLINE', '[25,35)'::int4range),
('dragarcia', 'ONLINE', '[20,30)'::int4range),
('leroyjenkins', 'ONLINE', '[20,40)'::int4range);
('leroyjenkins', 'ONLINE', '[20,40)'::int4range);

INSERT INTO shops(id, address, shop_geom)
VALUES
(1, '1369 Cambridge St', 'SRID=4326;POINT(-71.10044 42.373695)');
5 changes: 3 additions & 2 deletions test/db/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
version: '3'
services:
rest:
image: postgrest/postgrest:v9.0.0
image: postgrest/postgrest:v9.0.1.20220802
ports:
- '3000:3000'
environment:
PGRST_DB_URI: postgres://postgres:postgres@db:5432/postgres
PGRST_DB_SCHEMAS: public,personal
PGRST_DB_ANON_ROLE: postgres
PGRST_DB_PLAN_ENABLED: 1
depends_on:
- db
db:
image: postgres:12
image: postgis/postgis:12-3.2
ports:
- '5432:5432'
volumes:
Expand Down
121 changes: 121 additions & 0 deletions test/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,124 @@ test('abort signal', async () => {
}
`)
})

test('geojson', async () => {
const res = await postgrest
.from('shops')
.select()
.geojson()
.then((res) => res.data)
expect(res).toMatchInlineSnapshot(`
Object {
"features": Array [
Object {
"geometry": Object {
"coordinates": Array [
-71.10044,
42.373695,
],
"type": "Point",
},
"properties": Object {
"address": "1369 Cambridge St",
"id": 1,
},
"type": "Feature",
},
],
"type": "FeatureCollection",
}
`)
})

test('explain', async () => {
const res = await postgrest
.from('users')
.select()
.explain()
.then((res) => res.data)
expect(res).toMatchInlineSnapshot(`
Array [
Object {
"Plan": Object {
"Node Type": "Aggregate",
"Parallel Aware": false,
"Partial Mode": "Simple",
"Plan Rows": 1,
"Plan Width": 112,
"Plans": Array [
Object {
"Alias": "users",
"Node Type": "Seq Scan",
"Parallel Aware": false,
"Parent Relationship": "Outer",
"Plan Rows": 510,
"Plan Width": 132,
"Relation Name": "users",
"Startup Cost": 0,
"Total Cost": 15.1,
},
],
"Startup Cost": 17.65,
"Strategy": "Plain",
"Total Cost": 17.68,
},
},
]
`)
})

test('explain with options', async () => {
const res = await postgrest
.from('users')
.select()
.explain({ verbose: true, settings: true })
.then((res) => res.data)
expect(res).toMatchInlineSnapshot(`
Array [
Object {
"Plan": Object {
"Node Type": "Aggregate",
"Output": Array [
"NULL::bigint",
"count(ROW(users.username, users.data, users.age_range, users.status, users.catchphrase))",
"(COALESCE(json_agg(ROW(users.username, users.data, users.age_range, users.status, users.catchphrase)), '[]'::json))::character varying",
"NULLIF(current_setting('response.headers'::text, true), ''::text)",
"NULLIF(current_setting('response.status'::text, true), ''::text)",
],
"Parallel Aware": false,
"Partial Mode": "Simple",
"Plan Rows": 1,
"Plan Width": 112,
"Plans": Array [
Object {
"Alias": "users",
"Node Type": "Seq Scan",
"Output": Array [
"users.username",
"users.data",
"users.age_range",
"users.status",
"users.catchphrase",
],
"Parallel Aware": false,
"Parent Relationship": "Outer",
"Plan Rows": 510,
"Plan Width": 132,
"Relation Name": "users",
"Schema": "public",
"Startup Cost": 0,
"Total Cost": 15.1,
},
],
"Startup Cost": 17.65,
"Strategy": "Plain",
"Total Cost": 17.68,
},
"Settings": Object {
"search_path": "\\\"public\\\", \\\"public\\\"",
},
},
]
`)
})