Hasura — Scheduled Event Nedir?

Tahsin Safa Elmalı
10 min readJul 21, 2024

Bu yazının demo projesi GitHub’da bulunmaktadır — Demo projesinin bulunduğu GitHub Reposu → Repo

Bir mağazanız olduğunu ve müşterilerinize özel kampanya duyurularını belirli bir tarih ve saatte göndermek istediğinizi düşünün. Bu durumda, müşteri memnuniyetini artırmak ve satışları teşvik etmek için zamanında ve hedeflenmiş bildirimler göndermek kritik öneme sahip olabilir. Bu yazımızda, Hasura’nın Event Trigger ve Scheduled Trigger API’larını kullanarak bu işlemi nasıl gerçekleştirebileceğimizi ayrıntılı olarak inceleyeceğiz.

Eğer Hasura ile ilgili bir bilginiz yoksa, şu yazıyı incelemenizi öneririm — Hasura Nedir?

Öncelikle, Event Trigger ve Scheduled Trigger nedir bunlara bir bakalım.

Hasura — Event Trigger Nedir?

Hasura’nın Event Trigger özelliği, veritabanındaki belirli olaylar (örneğin, bir tabloya yeni bir kayıt eklenmesi, mevcut bir kaydın güncellenmesi veya silinmesi gibi) durumunda tetiklenen ve bu tetiklenme sonucunda ne yapılacağını belirleyebildiğimiz bir özelliktir. Örneğin, backend servisimizdeki bir endpoint’i çağırmak gibi işlemler gerçekleştirilebilir. Bu özellik, bir müşteri kaydı oluşturulduğunda bir e-posta gönderme veya bir sipariş güncellendiğinde stok seviyelerini güncelleme gibi işlemler için kullanılabilir.

Hasura — Scheduled Trigger Nedir?

Hasura’nın Scheduled Trigger özelliği, belirli bir zaman diliminde veya periyodik olarak belirli işlemleri tetiklemek için kullanılır. Bu özellik, rutin bakım görevlerini yürütmek, raporlar oluşturmak veya zamanlanmış bildirimler göndermek gibi işlemleri kolayca yönetmeyi sağlar. Örneğin, her gece yarısı veri yedeklemesi yapmak veya haftalık raporları oluşturup ilgili kişilere göndermek gibi işlemler Scheduled Trigger ile kolayca yönetilebilir. Ayrıca, belirli bir duyuru mesajının önceden belirlenmiş bir tarih ve saatte gönderilmesi de bu özellik sayesinde mümkündür.

Yapacağımız örneğin ana başlıklarından bahsedecek olursak;

1. Form Oluşturma

Bir form oluşturacağız. Bu formda iki ana alan olacak:

  • Mesaj Alanı: Kullanıcının mesajını yazabileceği bir input alanı.
  • Tarih ve Saat Seçimi Alanı: Kullanıcının mesajın gönderilmesini istediği tarih ve saati seçebileceği bir datepicker component’i.

Mesaj eklendiğinde, başlangıçtaki durumunu “scheduled” (zamanlandı) olarak belirleyeceğiz ve bu durumu bir ikon ile göstereceğiz.

2. Event Trigger Kurulumu

  • Hasura’da bulunan tabloya bir kayıt eklendiğinde, Event Trigger otomatik olarak çalışacak ve belirtilen backend endpoint’ine bir istek gönderecek.

3. Scheduled Trigger ile Zamanlama

  • Backend endpoint’i tetiklendiğinde, Hasura’nın Scheduled Trigger API’sini kullanarak seçilen tarih ve saate göre girilen mesajın zamanlanmasını sağlıyor olacağız.
  • Scheduled Trigger’ı, belirlenen tarih ve saate ulaşıldığında çalışacak şekilde ayarlayacağız.
  • Zamanlanmış mesajın saati geldiğinde, Scheduled Trigger ile ikinci bir endpoint’in çağrılmasını sağlayacağız.
  • Bu ikinci endpoint ile mesajın gönderilmesini sağlayacak fonksiyon yer alacak.

4. Mesajın Gönderilmesi

  • Bu ikinci endpoint, mesajın durumunu “sent (gönderildi)” olarak değiştirecek.
  • Mesajın gönderildiğini ve belirlenen saatte gönderildiğini göstermek için mesajın durumunu belirten bir ikon ekleyeceğiz.

Backend tarafı için Serverless Framework’ten yararlanıyor olacağız. Kısaca serverless tarafına da değinelim.

Serverless Nedir?

A serverless architecture is a way to build and run applications and services without having to manage infrastructure. Your application still runs on servers, but all the server management is done by AWS

Reference: https://aws.amazon.com/lambda/serverless-architectures-learn-more/#:~:text=A%20serverless%20architecture%20is%20a,management%20is%20done%20by%20AWS.

Serverless mimaride, geliştiriciler sunucu yönetimi ile uğraşmak zorunda kalmazlar. Uygulamanız hala sunucularda çalışır, ancak AWS gibi bulut sağlayıcıları tüm altyapıyı yönetir ve otomatik olarak ölçeklendirir. Bu, sadece kod tarafına odaklanmamızı sağlar ve altyapı yönetimi ile ilgili maliyetleri ve karmaşıklıkları azaltır.

Serverless Framework Nedir?

Serverless framework, bu sunucusuz mimariyi daha kolay ve hızlı bir şekilde kullanmamızı sağlayan bir araçtır.

Serverless framework, AWS Lambda, Google Cloud Functions ve Azure Functions gibi çeşitli bulut sağlayıcıları ile entegre çalışarak, fonksiyonlarımızı kolayca yönetmemize olanak tanır. Ayrıca, event tabanlı mimariyi destekleyerek belirli olaylara (örneğin, zamanlanmış görevler veya veri tabanı tetikleyicileri) bağlı olarak fonksiyonların çalıştırılmasını sağlar.

Şimdi, Hasura üzerinde zamanlanmış mesajlar oluşturmak için bu araçları nasıl kullanabileceğinizi adım adım inceleyeceğiz. Konu başlıklarımız sırasıyla:

  1. Serverless Projesini Oluşturulması
  2. Hasurada Tablonun Oluşturulması
  3. Arayüz Projesinin Oluşturulması
  4. Hasura’da Event Trigger’ın Oluşturulması
  5. Serverless Fonksiyonlarının Tanımlanması

Serverless — Part 1

Öncelikle serverless projemizi oluşturalım. Bu serverless kısmını uygulamamızın backend servisi gibi düşünebilirsiniz. Bir mesaj oluşturulduğu zaman bu mesajın zamanlanma işini bu fonksiyon içerisinde halledeceğiz. Şimdilik boş fonksiyonlarımızı oluşturalım ve bunu AWS’e deploy edelim. Böylelikle Hasura tarafında kullanabileceğimiz endpointlerimizi tanımlamış olacağız.

https://www.serverless.com/framework/docs/getting-started

Terminalimizde serverlesskomutunu çalıştırdığımızda, önümüze serverless’ı hangi dili kullanarak oluşturmak istediğimizle ilgili seçenekler çıkar. Bu yazımızda AWS — Node.js — HTTP API kullanıyor olacağız.

Creating a new serverless project

? What do you want to make? (Use arrow keys)
❯ AWS - Node.js - Starter
AWS - Node.js - HTTP API
AWS - Node.js - Scheduled Task
AWS - Node.js - SQS Worker
AWS - Node.js - Express API
AWS - Node.js - Express API with DynamoDB
AWS - Python - Starter
AWS - Python - HTTP API
AWS - Python - Scheduled Task
AWS - Python - SQS Worker
AWS - Python - Flask API
AWS - Python - Flask API with DynamoDB
Other

Seçimi yaptıktan sonra elimizde şöyle bir dosya yapısı olacak:<

my-serverless-project/
├── .gitignore
├── package.json
├── index.js
├── README.md
├── serverless.yml

Endpoint olarak kullanacağımız fonksiyonları tanımlayalım:

my-serverless-project/
├── .serverless/
├── node_modules/
├── src/
│ ├── functions/
│ │ ├── event-triggers/
│ │ │ └── message-created/
│ │ │ └── handler.ts
│ │ ├── scheduled-event-triggers/
│ │ │ └── scheduled-message-sent/
│ │ │ └── handler.ts
│ │ │ └── queries.ts
│ ├── utils/
│ │ └── hasura/
│ │ └── create-scheduled-event.ts
│ │ └── request.ts
├── .env
├── .gitignore
├── functions.yml
├── package.json
├── README.md
├── serverless.yml
├── tsconfig.json
├── yarn.lock

event-triggers/

Bu klasör, belirli olaylar gerçekleştiğinde tetiklenen fonksiyonları içerecektir. Örneğin, bir veritabanı tablosuna yeni bir kayıt eklendiğinde veya mevcut bir kayıt güncellendiğinde tetiklenen işlemleri bu klasörde bulundurabiliriz.

  • message-created/: Bu dosya, bir mesaj oluşturulduğunda tetiklenen fonksiyonu barındırır. Örneğin, yeni bir mesaj kaydı eklendiğinde çalışacak olan handler.ts dosyasını içerir.

scheduled-event-triggers/

Bu klasör, belirli bir zaman diliminde veya belirlenen zamanlarda çalışması gereken fonksiyonları içerir. Zamanlanmış görevler için kullanıyor olacağız.

  • scheduled-message-sent/: Bu klasör, zamanlanmış mesaj gönderme fonksiyonlarını içerir. Belirli bir zamanda mesaj göndermek için çalışacak olan handler.tsve ilgili veritabanı işlemleri için queries.tsdosyasını barındırır. message-created fonksiyonu içinde Hasura’nın scheduled event API’sini tetiklediğimizde, bu fonksiyonun belirlenen zamanda çalışmasını sağlayacağız.
serverless/src/functions/event-triggers/message-created/handler.ts

const mainFn = async (request: Request) => {
return {
message: "message-created event triggered",
};
};

module.exports.handle = mainFn;
serverless/src/functions/scheduled-event-triggers/scheduled-message-sent/handler.ts

const mainFn = async (request: Request) => {
return {
message: "Scheduled event triggered",
};
};

module.exports.handle = mainFn;

Bu fonksiyonlarımızı functions.yml içerisinde tanımlayalım.

scheduledMessageSent:
handler: src/functions/scheduled-event-triggers/scheduled-message-sent/handler.handle
events:
- httpApi:
path: /scheduled-message-sent
method: POST
messageCreated:
handler: src/functions/event-triggers/message-created/handler.handle
events:
- httpApi:
path: /message-created
method: POST

serverless yml içerisinde tanımlamış olduğumuz fonksiyonların yolunu belirtelim.

service: serverless-scheduled-event
frameworkVersion: "3"

provider:
name: aws
runtime: nodejs18.x
deploymentMethod: direct

functions: ${file(./functions.yml)}

plugins:
- serverless-plugin-typescript
- serverless-offline

Daha sonra sls deploy komutunu kullanarak AWS'e deployment'ı gerçekleştirebiliriz. Deployment gerçekleştikten sonra elimizde fonksiyonlarımız için oluşturulan bir API olacak.

Bu API'yı birazdan nasıl kullanacağımıza bakacağız.

Hasura — Part 1

Hasura tarafında yeni bir tablo ekleyelim. Bu tablonun ismi “message” olsun ve kullanıcılara gönderilecek veya gönderilen mesaj detaylarını içersin.

  • text: Kullanıcılara gönderilen veya gönderilecek olan mesajı içerir.
  • scheduled_at: Ayarlanan zaman değerini tutar.
  • status: “schedule” ve “sent” olarak iki değer bulunduracaktır. Bir mesaj zamanlandığında “schedule”, mesaj kullanıcıya gönderildiğinde ise “sent” olarak güncellenecektir.

Şimdi bir arayüz oluşturalım .

React

Oluşturduğumuz mesaj tablosuna kayıt eklemek için öncelikle basit bir form oluşturalım. Bu projemizde UI kütüphanesi olarak Material Tailwind kullanıyor olacağız. Form state management’ı için ise react-hook-form kullanalım. Tablomuzu oluşturduktan sonra Hasura’nın bize bu tablolara fetch ve mutation gibi API sağladığından bahsetmiştik. Hasura’nın API tarafına baktığımızda, mutation ile nasıl veri ekleyebileceğimizi görebiliriz.

Gerekli paketleri yükledikten ve Hasura ile olan bağlantımızı sağladıktan sonra Hasura’ya nasıl data ekleyebileceğimize bakalım:

const ADD_MESSAGE = gql`
mutation AddMessage($object: message_insert_input!) {
insert_message_one(object: $object) {
text
scheduled_at
status
}
}
`;

type ScheduleMessageModalProps = {
onClose: () => void;
};

export const ScheduleMessageModal: React.FC<ScheduleMessageModalProps> = ({
onClose,
}) => {
const [open, setOpen] = React.useState(false);
const [addMessage, { loading }] = useMutation(ADD_MESSAGE);
const {
register,
control,
handleSubmit,
reset,
formState: { errors },
} = useForm<Message>();

const handleModalToggle = () => {
setOpen(!open);
reset();
};

const onSubmit = async (data: Message) => {
addMessage({
variables: {
object: {
text: data.text,
scheduled_at: new Date().toISOString(),
status: "scheduled",
},
},
})
.then(() => {
onClose();
handleModalToggle();
})
.catch((error) => {
console.error(error);
});
};

Bir Modal içerisinde bir input alanı ve ne zaman bu mesajı göndereceğimizi belirten bir datepicker bulunmaktadır. Mesajı yazdıktan sonra tarihimizi seçip submit ettiğimizde, veri Hasura tarafında tablomuza kaydolur.

Şimdi bir veri kaydolduğunda Hasura tarafında nasıl backend tarafını tetikleyebileceğimize bakalım.

Hasura — Part 2

cloud.hasura.io üzerinden projemize giriş yaptıktan sonra, “Events” sekmesine tıklayalım. Burada eventler oluşturabiliriz. Bu eventler, elimizdeki bir tablo üzerinde bir değişiklik olduğunda belirlediğimiz endpoint’i tetiklerler.

Bir mesaj oluşturulduğunda, elimizdeki “message-created” fonksiyonunun serverless endpoint’ini tetikleyelim. “message-created” serverless fonksiyonun endpoint’ini bu event’imize verelim. Event’in çalıştığını görebilmek için Hasura üzerinden veya React uygulamamız üzerinden bir kayıt ekleyebiliriz.

Hemen test edelim:

Bir kayıt ekledikten sonra events kısmında message_created event’ini seçelim. Processed Events tarafında serverless fonksiyonumuzun tetiklendiğini görebilirsiniz.

Giden request’i ve response’u id’nin yanında bulunan ikona tıklayarak görebiliriz.

Gördüğünüz gibi girmiş olunan mesaj ve zaman, serverless fonksiyonumuza data nesnesi içerisinde gönderilmiştir.

Şimdi bu değerleri kullanarak, Serverless fonksiyonumuz içerisinde Hasura’nın scheduled API’sini kullanarak zaman ayarlı bir event oluşturalım.

Serverless — Part 2

Şimdi message-created fonksiyonumuzu yazmaya geçebiliriz.

serverless/src/functions/event-triggers/message-created/handler.ts

const mainFn = async (request: Request) => {
const {
event: {
data: { new: node },
},
}: EventData = JSON.parse(request.body);

await createScheduledEvent({
comment: "Scheduled message sent",
webhook:
"<SERVERLESS_URL>",
payload: {
messageId: node.id,
},
scheduleAt: node.scheduled_at,
});

return {
message: "Scheduled event triggered",
};
};

module.exports.handle = mainFn;

Request ve EventData tipleri, fonksiyonun alacağı veri yapılarını tanımlar.

mainFn, Hasura tarafından tetiklenen ve zamanlanmış bir etkinlik oluşturmak için createScheduledEvent fonksiyonunu çağıran ana fonksiyondur. Bu fonksiyon, gelen isteği request.body üzerinden alır, JSON formatında parse eder ve içindeki yeni veriyi (node) kullanarak bir zamanlanmış bir event oluşturur.

  • webhook parametresi, bir HTTP URL'sini temsil eder. Bu URL, belirli bir olay tetiklendiğinde ya da zamanlanmış bir etkinlik zamanı geldiğinde çağrılacak olan URL'dir.
  • scheduledAt parametresi, zamanlanmış bir etkinliğin ne zaman tetikleneceğini belirten bir zaman değeridir. Bu parametre, etkinliğin gerçekleşeceği zamanı belirtir.
  • payload parametresi, tetiklenen etkinlik sırasında gönderilecek olan veriyi temsil eder. Bu veri, event’in çağrıldığı URL'ye (webhook) gönderilir.
serverless/src/utils/hasura/create-scheduled-event.ts

const axios = require("axios");
const HASURA_URL = "<HASURA_ENDPOINT>";
const HASURA_ADMIN_SECRET = "<HASURA_ADMIN_SECRET>";

type CreateScheduledEventArgs = {
webhook: string;
scheduleAt: string;
payload: Record<string, unknown>;
headers?: Record<string, string>;
comment: string;
};

const createScheduledEvent = async ({
webhook,
scheduleAt,
payload,
headers,
comment,
}: CreateScheduledEventArgs) => {
console.log("Creating scheduled event...", {
webhook,
scheduleAt,
payload,
headers,
comment,
});
const response = await axios({
method: "post",
url: HASURA_URL,
headers: {
"Content-Type": "application/json",
"x-hasura-admin-secret": HASURA_ADMIN_SECRET,
},
data: {
type: "create_scheduled_event",
args: {
webhook,
schedule_at: scheduleAt,
payload,
comment,
},
},
}).catch((error) => {
console.error("Error creating scheduled event:", error);
});

const eventId = response.data.event_id;
const message = response.data.message;

return {
data: {
eventId,
message,
},
};
};

export { createScheduledEvent };

Bu fonksiyon, Hasura’nın API’sini kullanarak zamanlanmış bir event oluşturur. Parametre olarak webhook, scheduleAt, payload, headers ve comment alır. axios kütüphanesi ile Hasura'ya bir POST isteği gönderir ve zamanlanmış etkinliği oluşturur. Başarılı olduğunda, etkinlik ID'sini (eventId) ve mesajı (message) döner.

Bu zamanlanmış etkinlikleri Hasura’nın konsolundan inceleyebiliriz.

Events sekmesine tıkladığımızda, sağ tarafta One-off Scheduled Eventsseçeneği bulunmaktadır. Bu seçenek, henüz tetiklenmemiş ancak tetiklenmeyi bekleyen ve tetiklenmiş olan etkinlikleri gösterir.

Zamanı geldiğinde, buradaki event tetiklenir ve bizim ikinci endpoint’imiz olan scheduledMessageSent fonksiyonumuzu çağırır. Bu fonksiyon içerisinde neler yaptığımıza bakalım:

serverless/src/functions/scheduled-event-triggers/scheduled-message-sent/handler.ts

import { requestAsAdmin } from "../../../utils/hasura/request";
import { UpdateScheduledMessageStatus } from "./queries";

export interface Request {
body: string;
}

export interface Node {
payload: {
messageId: string;
};
}

const mainFn = async (request: Request) => {
console.log("Scheduled event triggered:", request);
const { payload }: Node = JSON.parse(request.body);

await requestAsAdmin(UpdateScheduledMessageStatus, {
id: payload.messageId,
}).catch((error) => {
console.error("Error updating scheduled message status:", error);
});

return {
message: "Scheduled event triggered",
};
};

module.exports.handle = mainFn;

Bu fonksiyonumuzda, Hasura’nın API’sini çağırırken eklediğimiz payload değerlerine erişebiliriz.

requestAsAdmin ile Hasura’ya admin olarak erişip, tablodaki veriyi değiştirebiliriz.

import { GraphQLClient, Variables } from "graphql-request";

export const client = new GraphQLClient(`${process.env.HASURA_GRAPHQL_URL}`);
export const requestAsAdmin = <T, V extends Variables = Variables>(
document: string,
variables: V
): Promise<T> => {
client.setHeader(
"x-hasura-admin-secret",
process.env.HASURA_ADMIN_SECRET as string
);
return client.request<T, V>(document, variables);
}

Yazının başında da belirttiğimiz gibi, zamanı gelen mesajı bu fonksiyon içerisinde kullanıcılara gönderebilir ve gönderildikten sonra mesajın durumunu “sent” olarak değiştirebilmemizi sağlayan mutation’ı tetikleyebiliriz.

serverless/src/functions/scheduled-event-triggers/scheduled-message-sent/queries.ts

import { gql } from "graphql-request";

export const UpdateScheduledMessageStatus = gql`
mutation UpdateScheduledMessageStatus($id: uuid!) {
update_message_by_pk(pk_columns: { id: $id }, _set: { status: "sent" }) {
id
}
}
`;

Bu değişiklikleri yapıp, serverless projemizi son haliyle deploy ettikten sonra;

  1. Arayüz projemiz üzerinden bir mesaj oluşturalım ve bir saat/tarih seçelim.
  2. Öncelikle message-created endpoint’imiz tetiklenir. Burada Hasura’nın scheduled event API’si kullanılarak, Hasura’ya ne zaman ve ne yapması gerektiğini belirtiriz.
  3. Bu örneğimizde, seçilen zaman/tarihe göre belirtilen endpoint tetiklenir.
  4. Payload kısmına girilen değerler, bu endpoint tetiklendiğinde gönderilir. İkinci tetiklenen endpoint içerisinde mesajı gönderdikten sonra durumu “sent (gönderildi)” olarak güncellenir!

Bu yazımızda Hasura’nın Scheduled Event API’nı kullanarak zamanlanmış mesajları nasıl gönderebileceğimize değinmiş olduk. Bir başka yazıda bu zamanlanmış mesajlar ile nasıl email gönderebiliriz ona bakıyor olacağız 🙌

Bu yazının referansları:

https://hasura.io/
https://www.serverless.com/

Umarım faydalı bir yazı olmuştur. Eksik veya yanlış olduğunu düşündüğünüz kısımları bana iletirseniz çok sevinirim :)

İletişim kanalları: TwitterLinkedInMail

Bir sonraki yazıda görüşmek üzere!

--

--