reference: media type handlers

This commit is contained in:
steve-chavez
2023-11-30 23:27:06 -05:00
committed by Steve Chavez
parent 62512268cd
commit d15e357d2e
12 changed files with 337 additions and 166 deletions
+234 -18
View File
@@ -1,29 +1,245 @@
.. _custom_media:
Media Type Handlers
###################
PostgREST offers builtin handlers for common media types such as ``application/json`` and ``text/csv``. You can add handlers for other media types or override the builtin handlers.
Media Type Handlers allows PostgREST to deliver custom media types. These handlers extend the :ref:`builtin ones <builtin_media>` and can also override them.
Standard Media Types Handlers
=============================
Media types are expressed as type aliases using `domains <https://www.postgresql.org/docs/current/sql-createdomain.html>`_ and their name must comply to `RFC 6838 requirements <https://datatracker.ietf.org/doc/html/rfc6838#section-4.2>`_.
Builtins handlers are provided for the following standard media types:
.. code-block:: postgres
* ``application/json``
* ``text/csv``
* ``application/geo+json``
* ``application/x-www-form-urlencoded``
* ``*/*``, uses the same handler as ``application/json``.
CREATE DOMAIN "application/json" AS json;
Vendor Media Types Handlers
===========================
By using these domains as return types:
PostgREST also includes its own vendored media types, handlers for these are provided but cannot be overridden.
- Of :ref:`Functions <s_procs>`, these will turn into handlers.
* ``application/vnd.pgrst.object``
* ``application/vnd.pgrst.array``
* ``application/vnd.pgrst.plan``
- Of `Aggregates <https://www.postgresql.org/docs/current/sql-createaggregate.html>`_ transition or final functions, these will serve as handlers for :ref:`tables_views` and :ref:`table_functions`.
Custom Media Type Handlers
==========================
.. note::
TODO
PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overriden in this way.
Handler Function
================
As an example, let's obtain the `TWKB <https://postgis.net/docs/ST_AsTWKB.html>`_ compressed binary format for a PostGIS geometry.
.. code-block:: postgres
create extension postgis;
create table lines (
id int primary key
, name text
, geom geometry(LINESTRING, 4326)
);
insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::extensions.geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::extensions.geometry);
For this you can create a vendor media type and use it as a return type on a function.
.. code-block:: postgres
create domain "application/vnd.twkb" as bytea;
create or replace function get_line (id int)
returns "application/vnd.twkb" as $$
select st_astwkb(geom) from lines where id = get_line.id;
$$ language sql;
.. note::
For PostgreSQL <= 12, you'll need a cast on the function body :code:`st_astwkb(geom)::"application/vnd.twkb"`.
Now you can request the ``TWKB`` output like so:
.. code-block:: bash
curl 'localhost:3000/rpc/get_line?id=1' -H "Accept: application/vnd.twkb" -i
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
Note that PostgREST will automatically set the ``Content-Type`` to ``application/vnd.twkb``.
Handlers for Tables/Views
=========================
To benefit from a compressed format like ``TWKB``, it makes more sense to obtain many rows instead of one. Let's allow that by adding a handler for the table. You'll need an aggregate:
.. code-block:: postgres
create or replace function twkb_handler_transition (state bytea, next lines)
returns "application/vnd.twkb" as $$
select state || st_astwkb(next.geom);
$$ language sql;
create or replace aggregate twkb_agg (lines) (
initcond = ''
, stype = "application/vnd.twkb"
, sfunc = twkb_handler_transition
);
-- quick test
-- SELECT twkb_agg(l) from lines l;
-- twkb_agg
------------------------------------------------------------------
-- \xa20002c09a0cc09a0c80ea3080ea30a2000280b51880b51880ea3080ea30
--(1 row)
Now you can request the table endpoint with the ``twkb`` media type:
.. code-block:: bash
curl 'localhost:3000/lines' -H "Accept: application/vnd.twkb" -i
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
If you have a table-valued function returning the same table type, the handler can also act upon on it.
.. code-block:: postgres
create or replace function get_lines ()
returns setof lines as $$
select * from lines;
$$ language sql;
.. code-block:: bash
curl 'localhost:3000/get_lines' -H "Accept: application/vnd.twkb" -i
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
Overriding a Builtin Handler
============================
Let's override the existing ``text/csv`` handler for the table to provide a more complex CSV output.
It'll include a `Byte order mark <https://en.wikipedia.org/wiki/Byte_order_mark>`_ plus a ``Content-Disposition`` header to set a name for the downloaded file.
.. code-block:: postgres
create domain "text/csv" as text;
create or replace function bom_csv_trans (state text, next lines)
returns "text/csv" as $$
select state || next.id::text || ',' || next.name || ',' || next.geom::text || E'\n';
$$ language sql;
create or replace function bom_csv_final (data "text/csv")
returns "text/csv" as $$
-- set the Content-Disposition header
select set_config('response.headers', '[{"Content-Disposition": "attachment; filename=\"lines.csv\""}]', true);
select
-- EFBBBF is the BOM in UTF8 https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
convert_from (decode (E'EFBBBF', 'hex'),'UTF8') ||
-- the header for the CSV
(E'id,name,geom\n' || data);
$$ language sql;
drop aggregate if exists bom_csv_agg(lines);
create aggregate bom_csv_agg (lines) (
initcond = ''
, stype = "text/csv"
, sfunc = bom_csv_trans
, finalfunc = bom_csv_final
);
You can now request it like:
.. code-block:: bash
curl 'localhost:3000/lines' -H "Accept: text/csv" -i
HTTP/1.1 200 OK
Content-Type: text/csv
Content-Disposition: attachment; filename="lines.csv"
id,name,geom
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840
.. _any_handler:
The "Any" Handler
=================
For more flexibility, you can also define a catch-all handler by using a domain named ``*/*`` (any media type). This will respond to all media types and even to requests that don't include an ``Accept`` header.
Note that this will take priority over all other handlers (builtin or custom), so it's better to do it for an isolated function or view.
Let's define an any handler for a view that will always respond with ``XML`` output. It will accept ``text/xml``, ``application/xml``, ``*/*`` and reject other media types.
.. code-block:: postgres
create domain "*/*" as pg_catalog.xml;
create view "lines.xml" as
select * from lines;
create or replace function lines_xml_trans (state "*/*", next "lines.xml")
returns "*/*" as $$
select xmlconcat(state, xmlelement(name line, xmlattributes(next.id as id, next.name as name), next.geom));
$$ language sql;
create or replace function lines_xml_final (data "*/*")
returns "*/*" as $$
declare
req_accept text := current_setting('request.headers', true)::json->>'accept';
begin
-- when receiving */*, we need to set the Content-Type. PostgREST won't set it.
if req_accept = '*/*'
then perform set_config('response.headers', '[{"Content-Type": "text/xml"}]', true);
-- we'll reject other non XML media types, we need to reject manually since */* will command PostgREST to accept all media types
elsif req_accept NOT IN ('application/xml', 'text/xml')
then raise sqlstate 'PT415' using message = 'Unsupported Media Type';
end if;
return data;
end; $$ language plpgsql;
drop aggregate if exists lines_xml_agg ("lines.xml");
create aggregate test.lines_xml_agg ("lines.xml") (
stype = "*/*"
, sfunc = lines_xml_trans
, finalfunc = lines_xml_final
);
Now we can omit the ``Accept`` header and it will respond with XML.
.. code-block:: bash
curl 'localhost:3000/lines.xml' -i
HTTP/1.1 200 OK
Content-Type: text/xml
<line id="1" name="line-1">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>
<line id="2" name="line-2">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>
And it will accept only XML media types.
.. code-block:: bash
curl 'localhost:3000/lines.xml' -i -H "Accept: text/xml"
HTTP/1.1 200 OK
Content-Type: text/xml
curl 'localhost:3000/lines.xml' -i -H "Accept: application/xml"
HTTP/1.1 200 OK
Content-Type: text/xml
curl 'localhost:3000/lines.xml' -i -H "Accept: unknown/media"
HTTP/1.1 415 Unsupported Media Type
+38 -73
View File
@@ -23,15 +23,44 @@ Use the Accept request header to specify the acceptable format (or formats) for
curl "http://localhost:3000/people" \
-H "Accept: application/json"
For tables and views the current possibilities are:
.. _builtin_media:
* ``*/*``
* ``text/csv``
* ``application/json``
* ``application/openapi+json``
* ``application/geo+json``
Builtin Media Type Handlers
===========================
The server will default to JSON for API endpoints and OpenAPI on the root.
Builtin handlers are offered for common standard media types.
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`s_procs`.
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
* ``application/geo+json``, see :ref:`ww_postgis`.
* ``*/*``, resolves to ``application/json`` for API endpoints and to ``application/openapi+json`` for the root endpoint.
The following vendor media types handlers are also supported.
* ``application/vnd.pgrst.plan``, see :ref:`explain_plan`.
* ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``, see :ref:`singular_plural` and :ref:`stripped_nulls`.
Any unrecognized media type will throw an error.
.. tabs::
.. code-tab:: http
GET /people HTTP/1.1
Accept: unknown/unknown
.. code-tab:: bash Curl
curl "http://localhost:3000/people" \
-H "Accept: unknown/unknown"
.. code-block:: http
HTTP/1.1 415 Unsupported Media Type
{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: unknown/unknown"}
To extend the accepted media types, you can use :ref:`custom_media`.
.. _singular_plural:
@@ -85,6 +114,8 @@ When a singular response is requested but no entries are found, the server respo
Admittedly PostgREST could detect when there is an equality condition holding on all columns constituting the primary key and automatically convert to singular. However this could lead to a surprising change of format that breaks unwary client code just by filtering on an extra column. Instead we allow manually specifying singular vs plural to decouple that choice from the URL format.
.. _stripped_nulls:
Stripped Nulls
--------------
@@ -122,72 +153,6 @@ This returns
{ "id": 13, "name": "Y"}
]
.. _scalar_return_formats:
Scalar Function Response Format
-------------------------------
In the special case of a :ref:`scalar_functions` there are three additional formats:
* ``application/octet-stream``
* ``text/plain``
* ``text/xml``
Example 1: If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header
and select a single column :code:`?select=bin_data`.
.. tabs::
.. code-tab:: http
GET /items?select=bin_data&id=eq.1 HTTP/1.1
Accept: application/octet-stream
.. code-tab:: bash Curl
curl "http://localhost:3000/items?select=bin_data&id=eq.1" \
-H "Accept: application/octet-stream"
Example 2: You can request XML output when having a scalar function that returns a type of ``text/xml``. You are not forced to use select for this case.
.. code-block:: postgres
CREATE FUNCTION generate_xml_content(..) RETURNS xml ..
.. tabs::
.. code-tab:: http
POST /rpc/generate_xml_content HTTP/1.1
Accept: text/xml
.. code-tab:: bash Curl
curl "http://localhost:3000/rpc/generate_xml_content" \
-X POST -H "Accept: text/xml"
Example 3: If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output.
.. code-block:: sql
CREATE FUNCTION get_descriptions(..) RETURNS SETOF TABLE(id int, description text) ..
.. tabs::
.. code-tab:: http
POST /rpc/get_descriptions?select=description HTTP/1.1
Accept: text/plain
.. code-tab:: bash Curl
curl "http://localhost:3000/rpc/get_descriptions?select=description" \
-X POST -H "Accept: text/plain"
.. note::
If more than one row would be returned the binary/plain-text/xml results will be concatenated with no delimiter.
.. _req_body:
Request Body
+4 -2
View File
@@ -311,7 +311,9 @@ Repeating also works in POST requests with ``Content-Type: application/x-www-for
-X POST -H "Content-Type: application/x-www-form-urlencoded" \
-d 'v=1&v=2&v=3&v=4'
Table-Valued functions
.. _table_functions:
Table-Valued Functions
----------------------
A function that returns a table type can be filtered using the same filters as :ref:`tables and views <tables_views>`. They can also use :ref:`Resource Embedding <s_proc_embed>`.
@@ -418,7 +420,7 @@ PostgREST will detect if the function is scalar or table-valued and will shape t
{ "title": "Blade Runner 2049", "rating": 8.1}
]
To manually choose a return format such as binary, plain text or XML, see the section :ref:`scalar_return_formats`.
To manually choose a return format such as binary, see :ref:`custom_media`.
.. _untyped_functions: