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 }