Top Banner
Relational DB to RESTful API
35

A Tour of PostgREST

Jan 21, 2018

Download

Technology

begriffs
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: A Tour of PostgREST

Relational DB to RESTful API

Page 2: A Tour of PostgREST

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?

??? ?

?

Page 3: A Tour of PostgREST

Taking the database seriously

Is PostgreSQL powerful and

flexible enough to replace

the custom API server?

That’s my experiment

Page 4: A Tour of PostgREST

The Traditional Web API Stack

Your App

Web Server

Database

Page 5: A Tour of PostgREST

The Traditional Web API Stack

Your App

Web Server

Database

PostgREST

A no-configuration canonical

mapping from DB to HTTP

Page 6: A Tour of PostgREST

Talk Overview

The Traditional API Server (brief)

Live demo of PostgREST

What’s the SQL? How did

it do that?

Page 7: A Tour of PostgREST

The Traditional App

Handmade Nested

Routes

Controllers

Imperative code

ORM

Logic divorced from

data

Page 8: A Tour of PostgREST

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...

Page 9: A Tour of PostgREST

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

Page 10: A Tour of PostgREST

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.

Page 11: A Tour of PostgREST

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

Page 12: A Tour of PostgREST

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

Page 13: A Tour of PostgREST

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

Page 14: A Tour of PostgREST
Page 15: A Tour of PostgREST

Security - Roles for Authorization

Anonymous Authenticator User(s)

Page 16: A Tour of PostgREST

Security - JWT for Authentication

YES

NO

Page 17: A Tour of PostgREST

Security - Roles in SQL

CREATE ROLE authenticator NOINHERIT LOGIN;

CREATE ROLE anon;

CREATE ROLE worker;

GRANT anon, worker TO authenticator;

Page 18: A Tour of PostgREST

Switching to a role

BEGIN ISOLATION LEVEL READ COMMITTED READ WRITE;

SET LOCAL ROLE 'worker';

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

-- ...

COMMIT;

Page 19: A Tour of PostgREST

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()

);

Page 20: A Tour of PostgREST

Let’s see it in action

Page 21: A Tour of PostgREST

External Actions

You can’t do everything

inside SQL

How do you

● Send an email?

● Call a 3rd party

service?

LISTEN / NOTIFY

Page 22: A Tour of PostgREST

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?

Page 23: A Tour of PostgREST

Use database schemas

Internal Schema V1

table1

table2

table3

view2

proc

view2

Page 24: A Tour of PostgREST

HTTP Interface is Flexible

postgrest --schema v1

postgrest --schema v2,v1

postgrest --schema v3,v2,v1

Accept: application/json;

version=2

ORGET /v2/...

Page 25: A Tour of PostgREST

Use the schema search-path

SET search_path TO v2, v1;

Page 26: A Tour of PostgREST

How does it work inside?

Warning: Boring / Cool

Page 27: A Tour of PostgREST

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

Page 28: A Tour of PostgREST

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

Page 29: A Tour of PostgREST

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

Page 30: A Tour of PostgREST

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

Page 31: A Tour of PostgREST

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

Page 32: A Tour of PostgREST

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

Page 33: A Tour of PostgREST

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

Page 34: A Tour of PostgREST

Learning More

Read the Docs

http://postgrest.com

Page 35: A Tour of PostgREST

github.com / begriffs / postgrest