Added webhook support and webhook display

This commit is contained in:
Borna Rajković 2023-08-06 14:22:32 +02:00
parent 89d246f3f6
commit 88d9188055
13 changed files with 551 additions and 26 deletions

View File

@ -22,29 +22,36 @@ CREATE TABLE IF NOT EXISTS "holiday"
references country(iso_name) on delete cascade on update cascade references country(iso_name) on delete cascade on update cascade
); );
INSERT INTO "country" (id, iso_name, name) CREATE TABLE IF NOT EXISTS "webhook"
VALUES (
('096ca6c4-5c04-47a4-0063-4b4cc6f6b671', 'HR', 'Croatia'), id uuid,
('096ca6c4-5c04-47a4-0063-4b4cc6f6b672', 'US', 'USA'), created timestamp NOT NULL,
('096ca6c4-5c04-47a4-0063-4b4cc6f6b673', 'FR', 'France'), url varchar(256) NOT NULL,
('096ca6c4-5c04-47a4-0063-4b4cc6f6b674', 'GB', 'Great Britain'); country char(2) NOT NULL,
retry_count int NOT NULL,
on_created bool NOT NULL,
on_edited bool NOT NULL,
on_deleted bool NOT NULL,
INSERT INTO "holiday" (id, country, date, name, description, is_state, is_religious) primary key (id),
VALUES constraint fk_country_id foreign key (country)
('096ca6c4-5c04-47a4-b363-4b4cc6f6b671', 'HR', '2023-01-01', 'Nova godina', '', true, false), references country(iso_name) on delete cascade on update cascade
('096ca6c4-5c04-47a4-b363-4b4cc6f6b672', 'HR', '2023-01-06', 'Sveta tri kralja', '', true, true), );
('096ca6c4-5c04-47a4-b363-4b4cc6f6b673', 'HR', '2023-04-09', 'Uskrs', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b674', 'HR', '2023-04-10', 'Uskrsni ponedjeljak', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b675', 'HR', '2023-05-01', 'Praznik rada', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b676', 'HR', '2023-05-30', 'Dan državnosti', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b677', 'HR', '2023-06-08', 'Tijelovo', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b678', 'HR', '2023-06-22', 'Dan antifašističke borbe', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b679', 'HR', '2023-08-05', 'Dan pobjede i domovinske zahvalnost', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b680', 'HR', '2023-08-15', 'Velika gospa', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b681', 'HR', '2023-11-01', 'Dan svih svetih', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b682', 'HR', '2023-11-18', 'Dan sjećanja na žrtve Domovinskog rata', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b683', 'HR', '2023-12-25', 'Božić', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b684', 'HR', '2023-12-26', 'Štefanje', '', true, true);
CREATE TABLE IF NOT EXISTS "job"
(
id uuid,
webhook_id uuid NOT NULL,
created timestamp NOT NULL,
success bool NOT NULL,
success_time timestamp,
retry_count int NOT NULL,
content varchar(1024) NOT NULL,
primary key (id),
constraint fk_webhook_id foreign key (webhook_id)
references webhook(id) on delete cascade on update cascade
);

33
db/dev/v1_1.sql Normal file
View File

@ -0,0 +1,33 @@
INSERT INTO "country" (id, iso_name, name)
VALUES
('096ca6c4-5c04-47a4-0063-4b4cc6f6b671', 'HR', 'Croatia'),
('096ca6c4-5c04-47a4-0063-4b4cc6f6b672', 'US', 'USA'),
('096ca6c4-5c04-47a4-0063-4b4cc6f6b673', 'FR', 'France'),
('096ca6c4-5c04-47a4-0063-4b4cc6f6b674', 'GB', 'Great Britain');
INSERT INTO "holiday" (id, country, date, name, description, is_state, is_religious)
VALUES
('096ca6c4-5c04-47a4-b363-4b4cc6f6b671', 'HR', '2023-01-01', 'Nova godina', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b672', 'HR', '2023-01-06', 'Sveta tri kralja', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b673', 'HR', '2023-04-09', 'Uskrs', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b674', 'HR', '2023-04-10', 'Uskrsni ponedjeljak', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b675', 'HR', '2023-05-01', 'Praznik rada', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b676', 'HR', '2023-05-30', 'Dan državnosti', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b677', 'HR', '2023-06-08', 'Tijelovo', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b678', 'HR', '2023-06-22', 'Dan antifašističke borbe', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b679', 'HR', '2023-08-05', 'Dan pobjede i domovinske zahvalnost', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b680', 'HR', '2023-08-15', 'Velika gospa', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b681', 'HR', '2023-11-01', 'Dan svih svetih', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b682', 'HR', '2023-11-18', 'Dan sjećanja na žrtve Domovinskog rata', '', true, false),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b683', 'HR', '2023-12-25', 'Božić', '', true, true),
('096ca6c4-5c04-47a4-b363-4b4cc6f6b684', 'HR', '2023-12-26', 'Štefanje', '', true, true);
INSERT INTO "webhook" (id, created, url, country, retry_count, on_created, on_edited, on_deleted)
VALUES
('b0b0a6c4-5c04-47a4-b363-4b4cc6f6b671', current_timestamp, 'https://hr.bbr-dev.info', 'HR', 10, true, true, true),
('b0b0a6c4-5c04-47a4-b363-4b4cc6f6b672', current_timestamp, 'https://hr2.bbr-dev.info', 'HR', 10, true, false, false);
INSERT INTO "job" (id, webhook_id, created, success, success_time, retry_count, content)
VALUES
('c0c0a6c4-5c04-47a4-b363-4b4cc6f6b671', 'b0b0a6c4-5c04-47a4-b363-4b4cc6f6b671', current_timestamp, false, null, 8, '{"type": "created", "holiday": {"id": "096ca6c4-5c04-47a4-b363-4b4cc6f6b671", "name": "Nova godina", "date": "2023-01-01"}}');

34
db/prod/v1_1.sql Normal file
View File

@ -0,0 +1,34 @@
CREATE TABLE IF NOT EXISTS "webhook"
(
id uuid,
created timestamp NOT NULL,
url varchar(256) NOT NULL,
country char(2) NOT NULL,
retry_count int NOT NULL,
on_created bool NOT NULL,
on_edited bool NOT NULL,
on_deleted bool NOT NULL,
primary key (id),
constraint fk_country_id foreign key (country)
references country(iso_name) on delete cascade on update cascade
);
CREATE TABLE IF NOT EXISTS "job"
(
id uuid,
webhook_id uuid NOT NULL,
created timestamp NOT NULL,
success bool NOT NULL,
success_time timestamp NOT NULL,
retry_count int NOT NULL,
content varchar(1024) NOT NULL,
primary key (id),
constraint fk_webhook_id foreign key (webhook_id)
references webhook(id) on delete cascade on update cascade
);

95
main.go
View File

@ -8,6 +8,7 @@ import (
_ "github.com/lib/pq" _ "github.com/lib/pq"
"holiday-api/holiday" "holiday-api/holiday"
"holiday-api/migration" "holiday-api/migration"
"holiday-api/webhook"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
@ -56,10 +57,15 @@ func main() {
holidayService := holiday.HolidayService{DB: client} holidayService := holiday.HolidayService{DB: client}
countryService := holiday.CountryService{DB: client} countryService := holiday.CountryService{DB: client}
yearService := holiday.YearService{DB: client} yearService := holiday.YearService{DB: client}
webhookService := webhook.WebhookService{
DB: client,
Events: make(chan webhook.Event, 10),
Authorization: "TODO", // TODO add authorization fetching
}
g.GET("/api/v1/holidays", getHolidays(holidayService)) g.GET("/api/v1/holidays", getHolidays(holidayService))
setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService) setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService, webhookService)
g.GET("/", func(c *gin.Context) { g.GET("/", func(c *gin.Context) {
year := time.Now().Year() year := time.Now().Year()
@ -121,18 +127,23 @@ func loadTemplates(g *gin.Engine) {
"templates/admin_dashboard.gohtml", "templates/admin_dashboard.gohtml",
"templates/countries.gohtml", "templates/countries.gohtml",
"templates/webhooks.gohtml",
"templates/dialogs/add-holiday.gohtml", "templates/dialogs/add-holiday.gohtml",
"templates/dialogs/edit-holiday.gohtml", "templates/dialogs/edit-holiday.gohtml",
"templates/dialogs/delete-holiday.gohtml", "templates/dialogs/delete-holiday.gohtml",
"templates/dialogs/check-is-a-holiday.gohtml", "templates/dialogs/check-is-a-holiday.gohtml",
"templates/dialogs/add-webhook.gohtml",
"templates/dialogs/edit-webhook.gohtml",
"templates/dialogs/delete-webhook.gohtml",
"templates/dialogs/edit-country.gohtml", "templates/dialogs/edit-country.gohtml",
"templates/dialogs/delete-country.gohtml", "templates/dialogs/delete-country.gohtml",
) )
} }
func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) { func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService, webhookService webhook.WebhookService) {
adminDashboard.Use(gin.BasicAuth(loadAuth())) adminDashboard.Use(gin.BasicAuth(loadAuth()))
adminDashboard.GET("/", func(c *gin.Context) { adminDashboard.GET("/", func(c *gin.Context) {
@ -252,10 +263,71 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida
c.Redirect(http.StatusSeeOther, "/admin/countries") c.Redirect(http.StatusSeeOther, "/admin/countries")
}) })
adminDashboard.GET("/webhooks", func(c *gin.Context) {
webhooks, _ := webhookService.Find()
c.HTML(http.StatusOK, "webhooks.gohtml", gin.H{"Webhooks": webhooks})
})
adminDashboard.POST("/webhooks", func(c *gin.Context) {
request := struct {
Id *string `form:"id"`
Url string `form:"url" binding:"required,min=1"`
RetryCount int `form:"retry_count" binding:"required,min=1,max=20"`
Country string `form:"country" binding:"len=2"`
OnCreated bool `form:"on_created"`
OnEdited bool `form:"on_edited"`
OnDeleted bool `form:"on_deleted"`
}{}
if err := c.ShouldBind(&request); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
hook := webhook.Webhook{
Url: request.Url,
Country: request.Country,
RetryCount: request.RetryCount,
OnCreated: request.OnCreated,
OnEdited: request.OnEdited,
OnDeleted: request.OnDeleted,
}
var err error
if request.Id != nil {
hook.Id = uuid.MustParse(*request.Id)
hook, err = webhookService.Update(hook)
} else {
hook.Id = uuid.Must(uuid.NewRandom())
hook.Created = time.Now()
hook, err = webhookService.Create(hook)
}
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
} else {
c.Redirect(http.StatusSeeOther, "/admin/webhooks")
}
})
adminDashboard.POST("/webhooks/:id/delete", func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
_, err := webhookService.FindById(id)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
if err := webhookService.Delete(id); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, "/admin/webhooks")
})
adminDashboard.GET("/dialogs/add-holiday", func(c *gin.Context) { adminDashboard.GET("/dialogs/add-holiday", func(c *gin.Context) {
countries, _ := countryService.Find() countries, _ := countryService.Find()
c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries}) c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries})
}) })
adminDashboard.GET("/dialogs/add-webhook", func(c *gin.Context) {
countries, _ := countryService.Find()
c.HTML(http.StatusOK, "add-webhook.gohtml", gin.H{"Countries": countries})
})
adminDashboard.GET("/dialogs/edit-holiday", func(c *gin.Context) { adminDashboard.GET("/dialogs/edit-holiday", func(c *gin.Context) {
id := uuid.MustParse(c.Query("id")) id := uuid.MustParse(c.Query("id"))
hol, err := service.FindById(id) hol, err := service.FindById(id)
@ -266,6 +338,16 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida
countries, _ := countryService.Find() countries, _ := countryService.Find()
c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol}) c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol})
}) })
adminDashboard.GET("/dialogs/edit-webhook", func(c *gin.Context) {
id := uuid.MustParse(c.Query("id"))
hook, err := webhookService.FindById(id)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
countries, _ := countryService.Find()
c.HTML(http.StatusOK, "edit-webhook.gohtml", gin.H{"Countries": countries, "Webhook": hook})
})
adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) { adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) {
id := uuid.MustParse(c.Query("id")) id := uuid.MustParse(c.Query("id"))
country, err := countryService.FindById(id) country, err := countryService.FindById(id)
@ -293,6 +375,15 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida
} }
c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country}) c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country})
}) })
adminDashboard.GET("/dialogs/delete-webhook", func(c *gin.Context) {
id := uuid.MustParse(c.Query("id"))
hook, err := webhookService.FindById(id)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
c.HTML(http.StatusOK, "delete-webhook.gohtml", gin.H{"Webhook": hook})
})
} }
func loadAuth() map[string]string { func loadAuth() map[string]string {

View File

@ -97,8 +97,7 @@ func executeMigration(db *sqlx.DB, name string, script string) error {
var err error = nil var err error = nil
if _, e := tx.Exec(script); e != nil { if _, e := tx.Exec(script); e != nil {
err = e err = e
} } else if _, e := tx.Exec(createMigration, name, hash(script)); e != nil {
if _, e := tx.Exec(createMigration, name, hash(script)); e != nil {
err = e err = e
} }
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@
<section class="container"> <section class="container">
<a class="selected" href="#">Search</a> <a class="selected" href="#">Search</a>
<a href="/admin/countries">Countries</a> <a href="/admin/countries">Countries</a>
<a href="/admin/webhooks">Webhooks</a>
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button> <button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
</section> </section>
</nav> </nav>

View File

@ -18,6 +18,7 @@
<section class="container"> <section class="container">
<a href="/admin">Search</a> <a href="/admin">Search</a>
<a class="selected" href="#">Countries</a> <a class="selected" href="#">Countries</a>
<a href="/admin/webhooks">Webhooks</a>
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button> <button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
</section> </section>
</nav> </nav>

View File

@ -0,0 +1,35 @@
<dialog class="card" id="create-card">
<h3 class="card-title">Create webhook</h3>
<form method="post" action="/admin/webhooks">
<section>
<label for="url">Url:</label>
<input required id="url" name="url" type="url">
</section>
<section>
<label for="country">Country:</label>
<select id="country" required name="country">
{{range $entry := .Countries}}
<option value="{{$entry.IsoName}}">{{$entry.Name}}</option>
{{end}}
</select>
</section>
<section>
<label for="retry_count">Retry count:</label>
<input required id="retry_count" name="retry_count" type="number" min="0" max="10">
</section>
<section>
<label class="checkbox"><span>On created</span><input id="on_created" value="true" name="on_created" type="checkbox"></label>
</section>
<section>
<label class="checkbox"><span>On edited</span><input id="on_edited" value="true" name="on_edited" type="checkbox"></label>
</section>
<section>
<label class="checkbox"><span>On deleted</span><input id="on_deleted" value="true" name="on_deleted" type="checkbox"></label>
</section>
<section class="actions">
<button type="submit">Create</button>
<button type="button" onclick="closeDialog('#create-card')">Cancel</button>
</section>
</form>
</dialog>

View File

@ -0,0 +1,11 @@
<dialog class="card" id="delete-card">
<h3 class="card-title">Delete webhook</h3>
<p>Are you sure you want to delete webhook to <br><b>{{.Webhook.Url}}?</b><br>All jobs for given webhook will be deleted!</p>
<form method="post" action="/admin/webhooks/{{.Webhook.Id}}/delete">
<section class="actions">
<button type="submit">Delete</button>
<button type="button" onclick="closeDialog('#delete-card')">Cancel</button>
</section>
</form>
</dialog>

View File

@ -0,0 +1,36 @@
<dialog class="card" id="update-card">
<h3 class="card-title">Edit webhook</h3>
<form method="post" action="/admin/webhooks">
<input type="hidden" id="id" name="id" value="{{.Webhook.Id}}">
<section>
<label for="url">Url:</label>
<input required id="url" name="url" type="url" value="{{.Webhook.Url}}">
</section>
<section>
<label for="country">Country:</label>
<select id="country" required name="country">
{{range $entry := .Countries}}
<option {{if eq $.Webhook.Country $entry.IsoName}}selected{{end}} value="{{$entry.IsoName}}">{{$entry.Name}}</option>
{{end}}
</select>
</section>
<section>
<label for="retry_count">Retry count:</label>
<input required id="retry_count" name="retry_count" type="number" min="0" max="10" value="{{.Webhook.RetryCount}}">
</section>
<section>
<label class="checkbox"><span>On created</span><input id="on_created" value="true" name="on_created" type="checkbox" {{if .Webhook.OnCreated}}checked{{end}}></label>
</section>
<section>
<label class="checkbox"><span>On edited</span><input id="on_edited" value="true" name="on_edited" type="checkbox" {{if .Webhook.OnEdited}}checked{{end}}></label>
</section>
<section>
<label class="checkbox"><span>On deleted</span><input id="on_deleted" value="true" name="on_deleted" type="checkbox" {{if .Webhook.OnDeleted}}checked{{end}}></label>
</section>
<section class="actions">
<button type="submit">Update</button>
<button type="button" onclick="closeDialog('#update-card')">Cancel</button>
</section>
</form>
</dialog>

63
templates/webhooks.gohtml Normal file
View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Holiday-api | Admin dashboard</title>
<link rel="stylesheet" href="/assets/style.css">
<script src="/assets/global.js"></script>
</head>
<body>
<div id="dialog-container"></div>
<header>
<section class="container">
<h1><a href="/">Holiday-api | Webhooks</a></h1>
</section>
</header>
<nav>
<section class="container">
<a href="/admin">Search</a>
<a href="/admin/countries">Countries</a>
<a class="selected" href="#">Webhooks</a>
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
</section>
</nav>
<main>
<section style="margin: 1em; flex-grow: 1">
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-webhook" style="margin-bottom: 1em; float: right">Create webhook</button>
<table style="width: 100%">
<thead>
<tr>
<th>Country</th>
<th>Url</th>
<th>Retry count</th>
<th>On create</th>
<th>On update</th>
<th>On delete</th>
<th></th>
</tr>
</thead>
<tbody>
{{range $entry := .Webhooks}}
<tr>
<td>{{$entry.Country}}</td>
<td>{{$entry.Url}}</td>
<td>{{$entry.RetryCount}}</td>
<td><img class="icon" src="{{if $entry.OnCreated}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
<td><img class="icon" src="{{if $entry.OnEdited}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
<td><img class="icon" src="{{if $entry.OnDeleted}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
<td>
<button type="button" data-type="dialog" data-trigger="#update-card" data-url="/admin/dialogs/edit-webhook?id={{$entry.Id}}" class="clean icon"><img class="icon" src="/assets/images/edit.svg"></button>
<button type="button" data-type="dialog" data-trigger="#delete-card" data-url="/admin/dialogs/delete-webhook?id={{$entry.Id}}" class="clean icon"><img class="icon" src="/assets/images/trash-delete.svg"></button>
</td>
</tr>
{{end}}
</tbody>
</table>
</section>
</main>
</body>
</html>

73
webhook/model.go Normal file
View File

@ -0,0 +1,73 @@
package webhook
import (
"encoding/json"
"github.com/google/uuid"
"time"
)
type EventType string
const (
TypeCreated EventType = "created"
TypeEdited EventType = "edited"
TypeDeleted EventType = "deleted"
)
type Event struct {
Type EventType `json:"type"`
Holiday struct {
Id uuid.UUID `json:"id"`
Name string `json:"name"`
Country string `json:"country"`
Description string `json:"description"`
Date time.Time `json:"date"`
IsStateHoliday bool `json:"is_state_holiday"`
IsReligiousHoliday bool `json:"is_religious_holiday"`
} `json:"holiday"`
}
func (e Event) Json() string {
content, err := json.Marshal(e)
if err != nil {
panic(err)
}
return string(content)
}
type Webhook struct {
Id uuid.UUID `db:"id"`
Created time.Time `db:"created"`
Url string `db:"url"`
Country string `db:"country"`
RetryCount int `db:"retry_count"`
OnCreated bool `db:"on_created"`
OnEdited bool `db:"on_edited"`
OnDeleted bool `db:"on_deleted"`
}
func (w Webhook) ShouldEmit(e Event) bool {
if w.Country == e.Holiday.Country {
if e.Type == TypeCreated && w.OnCreated {
return true
} else if e.Type == TypeEdited && w.OnEdited {
return true
} else if e.Type == TypeDeleted && w.OnDeleted {
return true
}
}
return false
}
type Job struct {
Id uuid.UUID `db:"id"`
WebhookId uuid.UUID `db:"webhook_id"`
Created time.Time `db:"created"`
Success bool `db:"success"`
SuccessTime *time.Time `db:"success_time"`
RetryCount int `db:"retry_count"`
Content string `db:"content"`
}

141
webhook/webhook_service.go Normal file
View File

@ -0,0 +1,141 @@
package webhook
import (
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"log"
"net/http"
"strings"
"time"
)
type WebhookService struct {
DB *sqlx.DB
Events <-chan Event
Authorization string
}
func (w *WebhookService) Listen() {
for event := range w.Events {
webhooks, _ := w.Find()
var usedWebhooks []Webhook
for _, webhook := range webhooks {
usedWebhooks = append(usedWebhooks, webhook)
}
w.CreateJobs(usedWebhooks, event)
}
}
func (w *WebhookService) Run() {
for {
jobs, err := w.findRunningJobs()
if err != nil {
log.Printf("failed fetching running jobs: %v", err)
}
for _, job := range jobs {
job := w.ExecuteJob(job)
if err := w.UpdateJob(job); err != nil {
log.Printf("failed updating job %v: %v", job.Id, err)
}
}
time.Sleep(2 * time.Hour)
}
}
func (w *WebhookService) Find() ([]Webhook, error) {
var webhooks []Webhook
return webhooks, w.DB.Select(&webhooks, `SELECT * FROM "webhook"`)
}
func (w *WebhookService) FindAllJobs() ([]Job, error) {
var jobs []Job
return jobs, w.DB.Select(&jobs, `SELECT * FROM "job"`)
}
func (w *WebhookService) FindJobs(webhookId uuid.UUID) ([]Job, error) {
var jobs []Job
return jobs, w.DB.Select(&jobs, `SELECT * FROM "job" WHERE id = $1`, webhookId)
}
func (w *WebhookService) findRunningJobs() ([]Job, error) {
var jobs []Job
return jobs, w.DB.Select(&jobs, `SELECT * FROM "job" WHERE retry_count > 0 ORDER BY created asc`)
}
func (w *WebhookService) Create(webhook Webhook) (Webhook, error) {
_, err := w.DB.Exec("INSERT INTO webhook (id, created, url, country, retry_count, on_created, on_edited, on_deleted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
&webhook.Id, &webhook.Created, &webhook.Url, &webhook.Country, &webhook.RetryCount, &webhook.OnCreated, &webhook.OnEdited, &webhook.OnDeleted,
)
return webhook, err
}
func (w *WebhookService) Update(webhook Webhook) (Webhook, error) {
_, err := w.DB.Exec("UPDATE webhook SET url = $2, country = $3, retry_count = $4, on_created = $5, on_edited = $6, on_deleted = $7 WHERE id = $1",
&webhook.Id, &webhook.Url, &webhook.Country, &webhook.RetryCount, &webhook.OnCreated, &webhook.OnEdited, &webhook.OnDeleted,
)
return webhook, err
}
func (w *WebhookService) CreateJobs(webhooks []Webhook, event Event) {
json := event.Json()
tx, _ := w.DB.Begin()
for _, webhook := range webhooks {
job := Job{
Id: uuid.Must(uuid.NewRandom()),
WebhookId: webhook.Id,
Created: time.Now(),
Success: false,
SuccessTime: nil,
RetryCount: webhook.RetryCount,
Content: json,
}
_, err := tx.Exec("INSERT INTO job (id, webhook_id, created, success, success_time, retry_count, content) VALUES ($1, $2, $3, $4, $5, $6, $7)",
&job.Id, &job.WebhookId, &job.Created, &job.Success, &job.SuccessTime, &job.RetryCount, &job.Content,
)
if err != nil {
tx.Rollback()
return
}
}
tx.Commit()
}
func (w *WebhookService) ExecuteJob(job Job) Job {
webhook, err := w.FindById(job.WebhookId)
if err != nil {
panic(err)
}
log.Printf("executing job [%d:%d] %v: POST %s", webhook.RetryCount-job.RetryCount, webhook.RetryCount, job.Id, webhook.Url)
request, err := http.NewRequest("POST", webhook.Url, strings.NewReader(job.Content))
request.Header.Add("authorization", "Api "+w.Authorization)
response, err := http.DefaultClient.Do(request)
if err != nil || response.StatusCode != http.StatusOK {
job.RetryCount--
} else {
job.Success = true
currentTime := time.Now()
job.SuccessTime = &currentTime
}
return job
}
func (w *WebhookService) FindById(webhookId uuid.UUID) (Webhook, error) {
var webhook Webhook
return webhook, w.DB.Get(&webhook, `SELECT * FROM "webhook" WHERE id = $1`, webhookId)
}
func (w *WebhookService) UpdateJob(job Job) error {
_, err := w.DB.Exec(`UPDATE job SET success = $2, success_time = $3, retry_count = $4 WHERE id = $1`,
&job.Id, &job.Success, &job.SuccessTime, &job.RetryCount,
)
return err
}
func (w *WebhookService) Delete(webhookId uuid.UUID) error {
_, err := w.DB.Exec(`DELETE FROM "webhook" WHERE id = $1`,
&webhookId,
)
return err
}