Berkan Yıldırım
Yazılım Geliştirme Mühendisi

Rapidrows: Postgres için Kolay API Server

Rapidrows: Postgres için Kolay API Server

RapidRows, PostgreSQL üzerinde sorgular çalıştırmak, scheduled job’lar gerçekleştirmek ve PostgreSQL bildirimlerini websocket’lara iletmek gibi kullanım durumlarına göre yapılandırılabilen açık kaynak kodlu C ve Go dillerinde yazılmış, bağımlılığı olmayan, düşük gecikmeli ve tek dosya olarak çalışan bir API sunucusudur.

Genel Bakış

RapidRows, low-code yapılandırılabilen bir API sunucusudur. PostgreSQL üzerindeki verileri okumak / işlemek amacıyla bir API sunucusu oluşturmanın en kolay yolu olmayı hedefliyor.

  • PostgreSQL için Tasarlandı: RapidRows, PostgreSQL ve sayısı giderek artan PostgreSQL uyumlu uygulamalar için tasarlanmıştır. Dahili connection pooler yapısına, PostgreSQL’ten gelen bildirimleri WebSockets ve Server Sent Events kanallarına iletme gibi özelliklere sahiptir.
  • Veritabanı Sorgulama Esnekliği Sağlar: Veritabanı şemasını inceleyerek otomatik bir REST veya GraphQL API sağlayan araçların aksine, RapidRows, SQL sorgularını kendi üzerinde çalıştırır. Bu, kullanıcıların özellikle OLAP kullanım durumları için karmaşık SQL sorguları yazmasını, test etmesini ve çalıştırmasını sağlar. Çok daha basit oluşturulan REST API’lar ile çalışılmasına olanak tanır.
  • Scheduled jobs: PostgreSQL veritabanı yönetiminde periyodik işler yürütmek çok kolaylaştırılmıştır. RapidRows, SQL sorguları ve JavaScript kodu çalıştırabilen CRON benzeri bir scheduler’a sahiptir. Partition’lar oluşturma, materialized view’ları yenileme vb. görevler planlanabilir.
  • Kabiliyetleri: Yapılandırılabilir CORS, yanıt sıkıştırma, parametre doğrulama, sorgu sonuçlarının sunucu tarafında önbelleğe alınması, sorgu zaman aşımı ayarlanabilmesi, transaction seçenekleri, connection pooling ve her bir endpoint için ayrı hata ayıklama günlüğü oluşturma. Ek doğrulamalar ve sorgu sonuçlarını değiştirebilmeniz için QuickJS JavaScript engine kullanımını sunar.
  • Öğrenimi Kolaydır: RapidRows, kolayca deploy edilebilir bir CLI aracıdır. Açık kaynaklı ve Apache 2.0 altında lisanslanmıştır. Herhangi bir PostgreSQL eklenti kurulumu gerektirmez.
  • Kullanımı Kolaydır: Bir JSON veya YAML dosyası ile yapılandırılır ve komut satırında yalnızca rapidrows komutu ile deploy edilir.

Mimari

https://rapidrows.io/

Nasıl Çalışır?

RapidRows’un en büyük parçası yapılandırılabilir HTTP sunucusudur. HTTP hizmetinde kullanılan yollar (paths) Swagger‘a çok benzer şekilde olmakla birlikte RapidRows yapılandırması bu yola nasıl yanıt verileceğini de belirtir. RapidRows terminolojisinde, yol bir endpoint’dir. Bir endpoint’teki yanıt şu şekillerde olabilir:

1- PostgreSQL sunucusunda bir sorgu çalıştırma ve sonucu JSON biçiminde döndürme,

2- Sorguyu çalıştırıp sonucu CSV biçiminde döndürme,

3- Sorguyu çalıştırıp yalnızca etkilenen satır sayısını dönderme (UPDATE gibi herhangi bir veri döndürmeyen sorgular için)

4- JavaScript kod yardımıyla gelen istekler kontrol edebilir, hangi sorguların çalıştırılacağına, hangi çıktının döndürüleceğine karar verebilir.

5- Text veya JSON statik veri döndürülebilir.

Parameters

Endpoint’ler parametrelere sahip olabilir. RapidRows parametreleri URI yolunun bir parçası, sorgu parametresi veya JSON ya da POST form body olarak verilebilir. Parametreler sayı (integer veya float), string, boolean, veya array olabilir. Array’ler yalnızca sayı, tamsayı, boolean içerebilir. Nested arrays ve nesneler desteklenmez.

Min/max değer, uzunluk ve array boyutu, regex ve enumerated liste gibi validation kuralları parametre özelinde belirtilebilir.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
version: '1'
endpoints:
- uri: /movies/by-genre-and-year
  implType: query-json
  script: |
    select F.title, C.name, F.release_year
      from film F
      join film_category FC on F.film_id = FC.film_id
      join category C on FC.category_id = C.category_id
     where C.name = any($1::text[])
       and F.release_year = $2   
  datasource: pagila
  params:
  - name: genres
    in: query
    type: array
    elemType: string
    minItems: 1
    required: true
  - name: year
    in: query
    type: integer
    minimum: 1952
    maximum: 2022
datasources:
- name: 'pagila'
  dbname: 'pagila'

Dataset: Pagila.

Parametre olarak verilen yıl ve türde vizyona giren filmler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ curl -i 'http://localhost:8080/movies/by-genre-and-year?genres=Sci-Fi&genres=Comedy&year=2006'
 
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 28 Feb 2023 06:29:57 GMT
Transfer-Encoding: chunked
 
{
  "rows": [
    [
      "AIRPLANE SIERRA",
      "Comedy",
      2006
    ],
    [
      "ANNIE IDENTITY",
      "Sci-Fi",
      2006
    ],
    [
      "ANTHEM LUKE",
      "Comedy",
      2006
    ],
    [
      "ATTACKS HATE",
      "Sci-Fi",
      2006
    ],
    
.
.
.

JavaScript API

JavaScript ortamı, ES2020 tarafından belirtilen tüm global nesnelerin yanı sıra RapidRows tarafından eklenen $sys adlı bir nesneye sahiptir. $sys nesnesi, bir endpoint veya scheduled job script olarak yürütülen JavaScript kodu tarafından kullanılabilir. Kullanım:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
version: '1'
endpoints:
- uri: /exec-js
  implType: javascript
  methods:
  - POST
  datasource: pagila
  script: |
    // get a connection to a datasource
    let conn = $sys.acquire("pagila");
 
    // perform a query
    let genreResult = conn.query(`
      select C.name
        from rental R
        join inventory I on R.inventory_id = I.inventory_id
        join film_category FC on I.film_id = FC.film_id
        join category C on C.category_id = FC.category_id
      where R.rental_id = $1
    `, $sys.params.rental_id);
 
    // check the result
    if (genreResult.rows.length != 1)
      throw "Rental not found";
    const genre = genreResult.rows[0][0];
 
    // further checks
    let today = (new Date()).getDay();
    if (genre == "Horror" && today == 3)
      throw "Cannot return Horror DVDs on Wednesdays!"
 
    // exec a SQL without a resultset
    conn.exec("UPDATE rental SET return_date = now() WHERE rental_id = $1",
      $sys.params.rental_id)   
  params:
  - name: rental_id
    in: body
    type: integer
    minimum: 1
    required: true
datasources:
- name: pagila
  dbname: pagila

Kullanımı:

1
2
3
curl -i -X POST http://localhost:8080/exec-js -H 'Content-Type: application/json' -d '{ "rental_id": 3 }'
HTTP/1.1 204 No Content
Date: Wed, 21 Sep 2022 06:07:17 GMT

Eğer olmayan bir rental_id gönderirsek:

1
2
3
4
5
6
7
8
curl -i -X POST http://localhost:8080/exec-js -H 'Content-Type: application/json' -d '{ "rental_id": 17000 }'
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf8
Date: Wed, 21 Sep 2022 06:07:24 GMT
Content-Length: 16
 
Rental not found

Yukarıdaki kodta belirli bir içerik türüne göre farklı hata üretmeyi denemek istersek

1
2
3
4
5
6
7
$ curl -i -X POST http://localhost:8080/exec-js -H 'Content-Type: application/json' -d '{ "rental_id": 4 }'
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf8
Date: Wed, 21 Sep 2022 06:07:14 GMT
Content-Length: 40

Cannot return Horror DVDs on Wednesdays!

Caching

Her endpoint, sonuçları sunucu tarafında belirli bir saniye boyunca önbelleğe alacak şekilde yapılandırılabilir. Bu kullanımda RapidRows, endpoint’in ilk çağrılmasında üretilen son yanıtı bellekte önbelleğe alır. Sonraki çağrı için, önbellek zaman aşımı süresi dolmamışsa aynı yanıtı yeniden kullanır.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: '1'
endpoints:
- uri: /param-in-body
  implType: query-json
  datasource: pagila
  script: |
        SELECT title, description FROM film WHERE fulltext @@ to_tsquery($1) ORDER BY title ASC
  params:
  - name: descfts
    in: body
    type: string
    required: true
  cache: 3600
datasources:
- name: pagila
  dbname: pagila

Transactions

Sorgular, endpoint yapılandırmasında transaction parametreleri belirtilerek bir transaction bağlamında çalıştırılabilir. PostgreSQL’de bir transaction read-only veya read-write olarak farklı ISO seviyelerinde (read-committed, repeatable-read or serializable) olabilir. Bir endpoint için transaction türü ayarlanarak SQL sorgusunun bu transaction içinde çalışması sağlanabilir.

Timeout

Endpoint çağrılarındaki sorgulamar için bir zaman aşımı belirtilebilir.

1
2
3
4
5
6
7
8
9
10
version: '1'
endpoints:
- uri: /query-timeout
  implType: query-json
  datasource: pagila
  script: SELECT pg_sleep(60)
  timeout: 5
datasources:
- name: pagila
  dbname: pagila

Datasources ve Connection Pooling

RapidRows’ta datasource bir PostgreSQL veritabanı bağlantısına karşılık gelir. Veri kaynağı isteğe bağlı olarak connection pooller ile yapılandırılabilir. Sorgular datasource ismiyle verilen veri kaynağında çalıştırılır. Bir datasource tanımlamasında tüm libpq connection parametreleri tanımlanabilir. Connection pooller, RapidRows başlatıldığında veritabanına minimum sayıda bağlantı kuracak şekilde yapılandırılabilir. Opsiyonel olarak maksimum bir limiti aşmayacak şekilde, kullanılmayan veya idle durumdaki bağlantıları kapatacak şekilde yapılandırılabilir.

1
2
3
4
5
6
7
8
9
10
datasources:
- name: pagila-dev
  dbname: pagila
  host: dev.proj.example.com
- name: pagila-prod
  dbname: pagila
  host: pgbouncer.prod.example.com
  params:
    application_name: rapidrows
    statement_timeout: 60

Notifications over WebSocket ve SSE

RapidRows, PostgreSQL channel’larına gönderilen bildirimleri WebSockets ve Server Sent Events‘e iletebilir. RapidRows terminolojisinde, bu tip Websocket ve SSE endpoint’ler streams olarak adlandırılır. Herhangi bir sayıda istemci, tek bir websocket/sse akışına bağlanabilir. NOTIFY veya pg_notify() kullanılarak gönderilen her bildirim stream’e bağlı istemcilere gönderilecektir.

1
2
3
4
5
6
7
8
9
version: '1'
streams:
- uri: '/new_payments'
  type: 'websocket'
  datasource: 'pagila'
  channel: 'payment_received'
datasources:
- name: 'pagila'
  dbname: 'pagila'

Scheduled jobs

RapidRows, scheduled job’ları çalıştırmak için CRON benzeri bir daemon sağlar . Benzer çözümlerin aksine veritabanı eklenti kurulumu gerektirmez ve veritabanında veri depolamaz. Job; görevin kendisi, SQL ifadeleri ya da JavaScript kodu olabilir. Schedule , standart CRON sözdizimi kullanılarak veya tekrarlama aralığı @every X şeklinde tanımlanabilir. X, saat, dakika ve saniye veya 10h3m5s gibi hepsi bir arada kullanılabilir. Endpoint’lere benzer şekilde, scheduled job’da kullanılan SQL deyimleri transaction seçeneklerine ve bir zaman aşımına sahip olabilir. JavaScript’de kullanarak gelecek ay için partition oluşturan bir job örneği:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
version: '1'
jobs:
- name: create-monthly-partition
  type: javascript
  schedule: '0 10 28 * *'
  datasource: pagila
  script: |
    // Sonraki 2 ay için YYYY ve MM bul
    const now = new Date();
    const nextMonth  = new Date(now.getFullYear(), now.getMonth()+1, 1);
    const next2Month = new Date(nextMonth.getFullYear(), nextMonth.getMonth()+1, 1);
    const y1 = nextMonth.getFullYear(), m1 = nextMonth.getMonth() + 1;
    const y2 = next2Month.getFullYear(), m2 = next2Month.getMonth() + 1;
    const m1s = (m1 < 10 ? '0' : '') + m1, m2s = (m2 < 10 ? '0' : '') + m2;
     
    // Bir sonraki ay için partition oluşturan sql hazırla
    const sql = `
    CREATE TABLE public.payment_p${y1}_${m1s} (
      payment_id integer DEFAULT nextval('public.payment_payment_id_seq'::regclass) NOT NULL,
      customer_id integer NOT NULL,
      staff_id integer NOT NULL,
      rental_id integer NOT NULL,
      amount numeric(5,2) NOT NULL,
      payment_date timestamp with time zone NOT NULL
    );
     
    ALTER TABLE ONLY public.payment
    ATTACH PARTITION public.payment_p${y1}_${m1s}
    FOR VALUES FROM ('${y1}-${m1s}-01 00:00:00+00') TO ('${y2}-${m2s}-01 00:00:00+00');`
     
    // sql'i çalıştır
    $sys.acquire('pagila').exec(sql)   
datasources:
- name: 'pagila'
  dbname: 'pagila'

CORS

Cross Origin Resource Sharing, root seviyede yapılandırılarak tüm stream ve endpoint’ler için uygulanabilir. Herhangi bir URI’da erişilen origin, method ve header’lar ayarlanabilir. Response header olarak dönülen değer de yapılandırılabilir:

  • Access-Control-Expose-Headers

  • Access-Control-Max-Age

  • Access-Control-Allow-Credentials

CORS ayarı belirtilmediğinde yanıtlarda CORS ile ilgili header’lar bulunmaz. CORS ayarı boş bir nesne olarak ayarlanırsa CORS, URI’lar için tüm erişimlere izin verecek şekilde ayarlanacaktır. Bu durum güvenlik problemlerine sebep olabilir.

Build and Install

RapidRows pre-built binary sürümleri GitHub‘da bulunabilir. Kurulum için Go compiler v1.18 veya üstü ve gcc/clang gerekir.

1
go install github.com/rapidloop/rapidrows/cmd/rapidrows@latest
1
2
$ rapidrows --yaml hello.yaml
2022-09-26 07:49:44.887 INF API server started successfully listen=127.0.0.1:8080
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=RapidRows service 1
After=network.target
 
[Service]
ExecStart=/usr/local/sbin/rapidrows path/to/config.json
WorkingDirectory=/
StandardOutput=append:/path/to/log/file
Restart=on-failure
RestartSec=5s
User=www-data
 
[Install]
WantedBy=multi-user.target

Kaynak

https://rapidrows.io/