A Tour of PostgREST

Post on 21-Jan-2018

5652 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

Transcript

Relational DB to RESTful API

Taking the database seriously

Most web frameworks treat

the DB as a dumb store

This helps give them broad

appeal

What if we take a stand

instead?

??? ?

?

Taking the database seriously

Is PostgreSQL powerful and

flexible enough to replace

the custom API server?

That’s my experiment

The Traditional Web API Stack

Your App

Web Server

Database

The Traditional Web API Stack

Your App

Web Server

Database

PostgREST

A no-configuration canonical

mapping from DB to HTTP

Talk Overview

The Traditional API Server (brief)

Live demo of PostgREST

What’s the SQL? How did

it do that?

The Traditional App

Handmade Nested

Routes

Controllers

Imperative code

ORM

Logic divorced from

data

What’s in an app?

HTTP request handling

Authentication

Authorization

Request Parsing

Request Validation

Database Communication

Database Response Handling

HTTP Response Building

With error handling woven

throughout...

Maintaining bespoke APIs gets old

“Most APIs look the

same, some have

icing, some have

fondant, some are

vanilla, some

chocolate. At the

core they’re all still

cakes.” -- Jett

Durham

Problem 1: Boilerplate

Want to add a new route?

Create model

Add each CRUD action

Check permissions

Support filtering, pagination

Special routes for joining data

New versions? More repetition.

Problem 2: No Single Source of Truth

Constraints are removed

from DB

No longer enforced

continuously + uniformly

Imperative code means

human must write docs

Authorization is per-

controller rather than

Problem 3: Hierarchy

Your info is relational, your routes

hierarchical

Say projects have parts and vice

versa.

Need routes for parts by project

and project by parts?

Other people recognize the

problem, hence GraphQL

Demo Time!

We’ll use the Pagila example database

It was ported from MySQL “Sakila”

It’s a DVD store with films, rentals, customers, payments,

categories, actors etc

Security - Roles for Authorization

Anonymous Authenticator User(s)

Security - JWT for Authentication

YES

NO

Security - Roles in SQL

CREATE ROLE authenticator NOINHERIT LOGIN;

CREATE ROLE anon;

CREATE ROLE worker;

GRANT anon, worker TO authenticator;

Switching to a role

BEGIN ISOLATION LEVEL READ COMMITTED READ WRITE;

SET LOCAL ROLE 'worker';

SET LOCAL "postgrest.claims.id" = 'jdoe';

-- ...

COMMIT;

Row-Level Security

PostgreSQL 9.5+ allows restricting access to individual rows

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

drop policy if exists authors_eigenedit on posts;

create policy authors_eigenedit on posts

using (true)

with check (

author = basic_auth.current_email()

);

Let’s see it in action

External Actions

You can’t do everything

inside SQL

How do you

● Send an email?

● Call a 3rd party

service?

LISTEN / NOTIFY

How to version the API?

So far OK but… but I don’t

want to couple the internal

schema with an API!

How to encapsulate true

schema?

How to version specific

endpoints?

Use database schemas

Internal Schema V1

table1

table2

table3

view2

proc

view2

HTTP Interface is Flexible

postgrest --schema v1

postgrest --schema v2,v1

postgrest --schema v3,v2,v1

Accept: application/json;

version=2

ORGET /v2/...

Use the schema search-path

SET search_path TO v2, v1;

How does it work inside?

Warning: Boring / Cool

Generating the payload in 100% SQL

WITH pg_source AS

(SELECT "public"."festival".* FROM "public"."festival")

SELECT

(SELECT pg_catalog.count(1) FROM "public"."festival") AS total_result_set,

pg_catalog.count(t) AS page_total,

NULL AS header,

array_to_json(array_agg(row_to_json(t)))::character VARYING AS body

FROM

(SELECT * FROM pg_source LIMIT ALL OFFSET 0) t

Adding a filter

WITH pg_source AS

(SELECT "public"."festival".* FROM "public"."festival"

WHERE "public"."festival"."name" LIKE '%fun%'::UNKNOWN)

SELECT

(SELECT pg_catalog.count(1) FROM "public"."festival"

WHERE "public"."festival"."name" LIKE '%fun%'::UNKNOWN) AS total_result_set,

pg_catalog.count(t) AS page_total,

NULL AS header,

array_to_json(array_agg(row_to_json(t)))::character varying AS body

FROM

(SELECT * FROM pg_source LIMIT ALL OFFSET 0) t

Optimistic cast

Or without a global count

WITH pg_source AS

(SELECT "public"."festival".* FROM "public"."festival")

SELECT

NULL AS total_result_set,

pg_catalog.count(t) AS page_total,

NULL AS header,

array_to_json(array_agg(row_to_json(t)))::character varying AS body

FROM

(SELECT * FROM pg_source LIMIT ALL OFFSET 0) t

Creating CSV body

-- ...

(SELECT string_agg(a.k, ',')

FROM

(SELECT json_object_keys(r)::TEXT AS k

FROM

(SELECT row_to_json(hh) AS r

FROM pg_source AS hh LIMIT 1) s

) a

) || '\n' ||

coalesce(

string_agg(

substring(t::text, 2, length(t::text) - 2), '\n'

), ''

)

-- ...

First row

Column names

Remove quotes

Embedding a relation

WITH pg_source AS

(SELECT "public"."film"."id", row_to_json("director".*) AS "director"

FROM "public"."film"

LEFT OUTER JOIN

(SELECT "public"."director".*

FROM "public"."director") AS "director"

ON "director"."name" = "film"."director")

SELECT

(SELECT pg_catalog.count(1) FROM "public"."film") AS total_result_set,

pg_catalog.count(t) AS page_total,

NULL AS header,

array_to_json(array_agg(row_to_json(t)))::character varying AS body

FROM

(SELECT * FROM pg_source LIMIT ALL OFFSET 0) t

Embed row as field

Key(s) detected

ELSE

CASE

WHEN t.typelem <> 0::oid AND t.typlen = (-1)

THEN 'ARRAY'::text

WHEN nt.nspname = 'pg_catalog'::name THEN

format_type(a.atttypid, NULL::integer)

ELSE 'USER-DEFINED'::text

END

END::information_schema.character_data AS data_type,

information_schema._pg_char_max_length(information_schema._pg_truetypid(a.*,

t.*), information_schema._pg_truetypmod(a.*,

t.*))::information_schema.cardinal_number AS character_maximum_length,

information_schema._pg_char_octet_length(information_schema._pg_truetypid(a.

*, t.*), information_schema._pg_truetypmod(a.*,

t.*))::information_schema.cardinal_number AS character_octet_length,

information_schema._pg_numeric_precision(information_schema._pg_truetypid(a.

*, t.*), information_schema._pg_truetypmod(a.*,

t.*))::information_schema.cardinal_number AS numeric_precision,

information_schema._pg_numeric_precision_radix(information_schema._pg_truety

pid(a.*, t.*), information_schema._pg_truetypmod(a.*,

t.*))::information_schema.cardinal_number AS numeric_precision_radix,

information_schema._pg_numeric_scale(information_schema._pg_truetypid(a.*,

t.*), information_schema._pg_truetypmod(a.*,

t.*))::information_schema.cardinal_number AS numeric_scale,

information_schema._pg_datetime_precision(information_schema._pg_truetypid(a

.*, t.*), information_schema._pg_truetypmod(a.*,

t.*))::information_schema.cardinal_number AS datetime_precision,

information_schema._pg_interval_type(information_schema._pg_truetypid(a.*,

t.*), information_schema._pg_truetypmod(a.*,

t.*))::information_schema.character_data AS interval_type,

NULL::integer::information_schema.cardinal_number AS

interval_precision,

NULL::character varying::information_schema.sql_identifier

AS character_set_catalog,

NULL::character varying::information_schema.sql_identifier

AS character_set_schema,

NULL::character varying::information_schema.sql_identifier

AS character_set_name,

SELECT DISTINCT

info.table_schema AS schema,

info.table_name AS table_name,

info.column_name AS name,

info.ordinal_position AS position,

info.is_nullable::boolean AS nullable,

info.data_type AS col_type,

info.is_updatable::boolean AS updatable,

info.character_maximum_length AS max_len,

info.numeric_precision AS precision,

info.column_default AS default_value,

array_to_string(enum_info.vals, ',') AS enum

FROM (

/*

-- CTE based on information_schema.columns to remove the owner filter

*/

WITH columns AS (

SELECT current_database()::information_schema.sql_identifier AS table_catalog,

nc.nspname::information_schema.sql_identifier AS table_schema,

c.relname::information_schema.sql_identifier AS table_name,

a.attname::information_schema.sql_identifier AS column_name,

a.attnum::information_schema.cardinal_number AS ordinal_position,

pg_get_expr(ad.adbin, ad.adrelid)::information_schema.character_data AS column_default,

CASE

WHEN a.attnotnull OR t.typtype = 'd'::"char" AND t.typnotnull THEN 'NO'::text

ELSE 'YES'::text

END::information_schema.yes_or_no AS is_nullable,

CASE

WHEN t.typtype = 'd'::"char" THEN

CASE

WHEN bt.typelem <> 0::oid AND bt.typlen = (-1) THEN 'ARRAY'::text

WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype,

NULL::integer)

ELSE 'USER-DEFINED'::text

END

Matching up foreign keys

Deleting an item

WITH pg_source AS

(DELETE FROM "test"."items"

WHERE "test"."items"."id" = '1'::unknown

RETURNING "test"."items".*)

SELECT

'' AS total_result_set,

pg_catalog.count(t) AS page_total,

'',

''

FROM

(SELECT 1 FROM pg_source) t

Learning More

Read the Docs

http://postgrest.com

github.com / begriffs / postgrest

top related