package model
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/lib/pq"
"sourcecraft.dev/bigbes/sr-ht-core/database"
coremodel "sourcecraft.dev/bigbes/sr-ht-core/model"
)
// WebhookDelivery is a record of a single webhook delivery attempt. It is
// hand-written (rather than generated) so it can satisfy database.Model:
// gqlgen autobinds it by name, and the field maps below are what the core-go
// query builder selects and scans through.
//
// Name is the table prefix — "user" here — so Table() resolves to
// gql_user_wh_delivery. It is not a GraphQL field; it is carried so a delivery
// loaded in a listing knows which subscription table to load its subscription
// from.
type WebhookDelivery struct {
UUID string `json:"uuid"`
Date time.Time `json:"date"`
Event WebhookEvent `json:"event"`
RequestBody string `json:"requestBody"`
ResponseBody *string `json:"responseBody"`
ResponseHeaders *string `json:"responseHeaders"`
ResponseStatus *int `json:"responseStatus"`
ID int
SubscriptionID int
Name string
alias string
fields *database.ModelFields
}
func (whd *WebhookDelivery) WithName(name string) *WebhookDelivery {
whd.Name = name
return whd
}
func (whd *WebhookDelivery) As(alias string) *WebhookDelivery {
whd.alias = alias
return whd
}
func (whd *WebhookDelivery) Alias() string {
return whd.alias
}
func (whd *WebhookDelivery) Table() string {
return "gql_" + whd.Name + "_wh_delivery"
}
func (whd *WebhookDelivery) Fields() *database.ModelFields {
if whd.fields != nil {
return whd.fields
}
whd.fields = &database.ModelFields{
Fields: []*database.FieldMap{
{SQL: "uuid", GQL: "uuid", Ptr: &whd.UUID},
{SQL: "date", GQL: "date", Ptr: &whd.Date},
{SQL: "event", GQL: "event", Ptr: &whd.Event},
{SQL: "request_body", GQL: "requestBody", Ptr: &whd.RequestBody},
{SQL: "response_body", GQL: "responseBody", Ptr: &whd.ResponseBody},
{SQL: "response_headers", GQL: "responseHeaders", Ptr: &whd.ResponseHeaders},
{SQL: "response_status", GQL: "responseStatus", Ptr: &whd.ResponseStatus},
// Always fetch:
{SQL: "id", GQL: "", Ptr: &whd.ID},
{SQL: "subscription_id", GQL: "", Ptr: &whd.SubscriptionID},
},
}
return whd.fields
}
func (whd *WebhookDelivery) QueryWithCursor(ctx context.Context,
runner sq.BaseRunner, q sq.SelectBuilder,
cur *coremodel.Cursor) ([]*WebhookDelivery, *coremodel.Cursor) {
var (
err error
rows *sql.Rows
)
if cur.Next != "" {
next, _ := strconv.ParseInt(cur.Next, 10, 64)
q = q.Where(database.WithAlias(whd.alias, "id")+"<= ?", next)
}
q = q.
OrderBy(database.WithAlias(whd.alias, "id") + " DESC").
Limit(uint64(cur.Count + 1))
if rows, err = q.RunWith(runner).QueryContext(ctx); err != nil {
panic(err)
}
defer rows.Close()
var deliveries []*WebhookDelivery
for rows.Next() {
var delivery WebhookDelivery
if err := rows.Scan(database.Scan(ctx, &delivery)...); err != nil {
panic(err)
}
delivery.Name = whd.Name
deliveries = append(deliveries, &delivery)
}
if len(deliveries) > cur.Count {
cur = &coremodel.Cursor{
Count: cur.Count,
Next: strconv.Itoa(deliveries[len(deliveries)-1].ID),
Search: cur.Search,
}
deliveries = deliveries[:cur.Count]
} else {
cur = nil
}
return deliveries, cur
}
// UserWebhookSubscription is a user-scoped webhook subscription. spec.sr.ht is
// single-owner, so the OAuth client field pages.sr.ht carries is dropped from
// the schema; the client_id column still exists in the DB and is loaded here as
// an always-fetch, non-GraphQL field so FilterWebhooks and the auth-config
// round-trip keep working unchanged.
type UserWebhookSubscription struct {
ID int `json:"id"`
Events []WebhookEvent `json:"events"`
Query string `json:"query"`
URL string `json:"url"`
UserID int
AuthMethod string
ClientID *string
TokenHash *string
Expires *time.Time
Grants *string
NodeID *string
alias string
fields *database.ModelFields
}
// Scan lets a WebhookEvent be read straight out of a pq.Array column.
func (we *WebhookEvent) Scan(src any) error {
bytes, ok := src.([]uint8)
if !ok {
return fmt.Errorf("unable to scan from %T into WebhookEvent", src)
}
*we = WebhookEvent(string(bytes))
if !we.IsValid() {
return fmt.Errorf("%s is not a valid WebhookEvent", string(bytes))
}
return nil
}
func (UserWebhookSubscription) IsWebhookSubscription() {}
func (sub *UserWebhookSubscription) As(alias string) *UserWebhookSubscription {
sub.alias = alias
return sub
}
func (sub *UserWebhookSubscription) Alias() string {
return sub.alias
}
func (sub *UserWebhookSubscription) Table() string {
return "gql_user_wh_sub"
}
func (sub *UserWebhookSubscription) Fields() *database.ModelFields {
if sub.fields != nil {
return sub.fields
}
sub.fields = &database.ModelFields{
Fields: []*database.FieldMap{
{SQL: "events", GQL: "events", Ptr: pq.Array(&sub.Events)},
{SQL: "url", GQL: "url", Ptr: &sub.URL},
// Always fetch:
{SQL: "id", GQL: "", Ptr: &sub.ID},
{SQL: "query", GQL: "", Ptr: &sub.Query},
{SQL: "user_id", GQL: "", Ptr: &sub.UserID},
{SQL: "auth_method", GQL: "", Ptr: &sub.AuthMethod},
{SQL: "token_hash", GQL: "", Ptr: &sub.TokenHash},
{SQL: "client_id", GQL: "", Ptr: &sub.ClientID},
{SQL: "grants", GQL: "", Ptr: &sub.Grants},
{SQL: "expires", GQL: "", Ptr: &sub.Expires},
{SQL: "node_id", GQL: "", Ptr: &sub.NodeID},
},
}
return sub.fields
}
func (sub *UserWebhookSubscription) QueryWithCursor(ctx context.Context,
runner sq.BaseRunner, q sq.SelectBuilder,
cur *coremodel.Cursor) ([]WebhookSubscription, *coremodel.Cursor) {
var (
err error
rows *sql.Rows
)
if cur.Next != "" {
next, _ := strconv.ParseInt(cur.Next, 10, 64)
q = q.Where(database.WithAlias(sub.alias, "id")+"<= ?", next)
}
q = q.
OrderBy(database.WithAlias(sub.alias, "id")).
Limit(uint64(cur.Count + 1))
if rows, err = q.RunWith(runner).QueryContext(ctx); err != nil {
panic(err)
}
defer rows.Close()
var (
subs []WebhookSubscription
lastID int
)
for rows.Next() {
var sub UserWebhookSubscription
if err := rows.Scan(database.Scan(ctx, &sub)...); err != nil {
panic(err)
}
subs = append(subs, &sub)
lastID = sub.ID
}
if len(subs) > cur.Count {
cur = &coremodel.Cursor{
Count: cur.Count,
Next: strconv.Itoa(lastID),
Search: cur.Search,
}
subs = subs[:cur.Count]
} else {
cur = nil
}
return subs, cur
}