api/graph: Implement GraphQL-native tracker webhooks
Implement GraphQL-native tracker webhooks for tracker, label, and ticket CRUD operations.
This commit is contained in:
parent
467dd4075f
commit
f495ccdf32
|
@ -22,3 +22,5 @@ require (
|
|||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
)
|
||||
|
||||
replace github.com/Masterminds/squirrel => github.com/lieut-data/squirrel v1.5.4
|
||||
|
|
|
@ -46,8 +46,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/Masterminds/squirrel v1.4.0 h1:he5i/EXixZxrBUWcxzDYMiju9WZ3ld/l7QBNuo/eN3w=
|
||||
github.com/Masterminds/squirrel v1.4.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 h1:XcF0cTDJeiuZ5NU8w7WUDge0HRwwNRmxj/GGk6KSA6g=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
|
@ -216,6 +214,8 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR
|
|||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lieut-data/squirrel v1.5.4 h1:OGzJNl0/ZxdjLEHuFzDo797zB2V7i8wQXBVThcOzbHE=
|
||||
github.com/lieut-data/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
|
|
|
@ -233,6 +233,7 @@ func (ev *Event) Fields() *database.ModelFields {
|
|||
|
||||
// Always fetch:
|
||||
{"id", "", &ev.ID},
|
||||
{"created", "", &ev.Created},
|
||||
{"event_type", "", &ev.EventType},
|
||||
{"participant_id", "", &ev.ParticipantID},
|
||||
{"ticket_id", "", &ev.TicketID},
|
||||
|
|
|
@ -231,3 +231,110 @@ func (sub *UserWebhookSubscription) QueryWithCursor(ctx context.Context,
|
|||
|
||||
return subs, cur
|
||||
}
|
||||
|
||||
type TrackerWebhookSubscription struct {
|
||||
ID int `json:"id"`
|
||||
Events []WebhookEvent `json:"events"`
|
||||
Query string `json:"query"`
|
||||
URL string `json:"url"`
|
||||
TrackerID int `json:"trackerId"`
|
||||
|
||||
UserID int
|
||||
AuthMethod string
|
||||
ClientID *string
|
||||
TokenHash *string
|
||||
Expires *time.Time
|
||||
Grants *string
|
||||
NodeID *string
|
||||
|
||||
alias string
|
||||
fields *database.ModelFields
|
||||
}
|
||||
|
||||
func (TrackerWebhookSubscription) IsWebhookSubscription() {}
|
||||
|
||||
func (sub *TrackerWebhookSubscription) As(alias string) *TrackerWebhookSubscription {
|
||||
sub.alias = alias
|
||||
return sub
|
||||
}
|
||||
|
||||
func (sub *TrackerWebhookSubscription) Alias() string {
|
||||
return sub.alias
|
||||
}
|
||||
|
||||
func (sub *TrackerWebhookSubscription) Table() string {
|
||||
return "gql_tracker_wh_sub"
|
||||
}
|
||||
|
||||
func (sub *TrackerWebhookSubscription) Fields() *database.ModelFields {
|
||||
if sub.fields != nil {
|
||||
return sub.fields
|
||||
}
|
||||
sub.fields = &database.ModelFields{
|
||||
Fields: []*database.FieldMap{
|
||||
{"events", "events", pq.Array(&sub.Events)},
|
||||
{"url", "url", &sub.URL},
|
||||
|
||||
// Always fetch:
|
||||
{"id", "", &sub.ID},
|
||||
{"query", "", &sub.Query},
|
||||
{"user_id", "", &sub.UserID},
|
||||
{"auth_method", "", &sub.AuthMethod},
|
||||
{"token_hash", "", &sub.TokenHash},
|
||||
{"client_id", "", &sub.ClientID},
|
||||
{"grants", "", &sub.Grants},
|
||||
{"expires", "", &sub.Expires},
|
||||
{"node_id", "", &sub.NodeID},
|
||||
{"tracker_id", "", &sub.TrackerID},
|
||||
},
|
||||
}
|
||||
return sub.fields
|
||||
}
|
||||
|
||||
func (sub *TrackerWebhookSubscription) QueryWithCursor(ctx context.Context,
|
||||
runner sq.BaseRunner, q sq.SelectBuilder,
|
||||
cur *model.Cursor) ([]WebhookSubscription, *model.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 TrackerWebhookSubscription
|
||||
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 = &model.Cursor{
|
||||
Count: cur.Count,
|
||||
Next: strconv.Itoa(lastID),
|
||||
Search: cur.Search,
|
||||
}
|
||||
subs = subs[:cur.Count]
|
||||
} else {
|
||||
cur = nil
|
||||
}
|
||||
|
||||
return subs, cur
|
||||
}
|
||||
|
|
|
@ -137,6 +137,18 @@ type Tracker {
|
|||
archive of the tracker.
|
||||
"""
|
||||
export: URL!
|
||||
|
||||
"""
|
||||
Returns a list of tracker webhook subscriptions. For clients
|
||||
authenticated with a personal access token, this returns all webhooks
|
||||
configured by all GraphQL clients for your account. For clients
|
||||
authenticated with an OAuth 2.0 access token, this returns only webhooks
|
||||
registered for your client.
|
||||
"""
|
||||
webhooks(cursor: Cursor): WebhookSubscriptionCursor!
|
||||
|
||||
"Returns details of a tracker webhook subscription by its ID."
|
||||
webhook(id: Int!): WebhookSubscription
|
||||
}
|
||||
|
||||
type OAuthClient {
|
||||
|
@ -148,6 +160,11 @@ enum WebhookEvent {
|
|||
TRACKER_UPDATE @access(scope: TRACKERS, kind: RO)
|
||||
TRACKER_DELETED @access(scope: TRACKERS, kind: RO)
|
||||
TICKET_CREATED @access(scope: TICKETS, kind: RO)
|
||||
TICKET_UPDATE @access(scope: TICKETS, kind: RO)
|
||||
LABEL_CREATED @access(scope: TRACKERS, kind: RO)
|
||||
LABEL_UPDATE @access(scope: TRACKERS, kind: RO)
|
||||
LABEL_DELETED @access(scope: TRACKERS, kind: RO)
|
||||
EVENT_CREATED @access(scope: EVENTS, kind: RO)
|
||||
}
|
||||
|
||||
interface WebhookSubscription {
|
||||
|
@ -179,6 +196,18 @@ type UserWebhookSubscription implements WebhookSubscription {
|
|||
sample(event: WebhookEvent): String!
|
||||
}
|
||||
|
||||
type TrackerWebhookSubscription implements WebhookSubscription {
|
||||
id: Int!
|
||||
events: [WebhookEvent!]!
|
||||
query: String!
|
||||
url: String!
|
||||
client: OAuthClient @private
|
||||
deliveries(cursor: Cursor): WebhookDeliveryCursor!
|
||||
sample(event: WebhookEvent!): String!
|
||||
|
||||
tracker: Tracker!
|
||||
}
|
||||
|
||||
type WebhookDelivery {
|
||||
uuid: String!
|
||||
date: Time!
|
||||
|
@ -219,6 +248,22 @@ type TicketEvent implements WebhookPayload {
|
|||
ticket: Ticket!
|
||||
}
|
||||
|
||||
type EventCreated implements WebhookPayload {
|
||||
uuid: String!
|
||||
event: WebhookEvent!
|
||||
date: Time!
|
||||
|
||||
newEvent: Event!
|
||||
}
|
||||
|
||||
type LabelEvent implements WebhookPayload {
|
||||
uuid: String!
|
||||
event: WebhookEvent!
|
||||
date: Time!
|
||||
|
||||
label: Label!
|
||||
}
|
||||
|
||||
enum TicketStatus {
|
||||
REPORTED
|
||||
CONFIRMED
|
||||
|
@ -728,6 +773,12 @@ input UserWebhookInput {
|
|||
query: String!
|
||||
}
|
||||
|
||||
input TrackerWebhookInput {
|
||||
url: String!
|
||||
events: [WebhookEvent!]!
|
||||
query: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
"""
|
||||
Creates a new bug tracker. If specified, the 'import' field specifies a
|
||||
|
@ -860,7 +911,7 @@ type Mutation {
|
|||
may be used to access details of the event which trigged the webhook. The
|
||||
query may not make any mutations.
|
||||
"""
|
||||
createWebhook(config: UserWebhookInput!): WebhookSubscription!
|
||||
createUserWebhook(config: UserWebhookInput!): WebhookSubscription!
|
||||
|
||||
"""
|
||||
Deletes a user webhook. Any events already queued may still be
|
||||
|
@ -870,5 +921,11 @@ type Mutation {
|
|||
Manually deleting a webhook configured by a third-party client may cause
|
||||
unexpected behavior with the third-party integration.
|
||||
"""
|
||||
deleteWebhook(id: Int!): WebhookSubscription
|
||||
deleteUserWebhook(id: Int!): WebhookSubscription
|
||||
|
||||
"Creates a new tracker webhook."
|
||||
createTrackerWebhook(trackerId: Int!, config: TrackerWebhookInput!): WebhookSubscription!
|
||||
|
||||
"Deletes a tracker webhook."
|
||||
deleteTrackerWebhook(id: Int!): WebhookSubscription
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
"git.sr.ht/~sircmpwn/todo.sr.ht/api/webhooks"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
|
@ -193,7 +194,7 @@ func (r *mutationResolver) CreateTracker(ctx context.Context, name string, descr
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyTrackerEvent(ctx, &tracker, "tracker:create")
|
||||
webhooks.DeliverTrackerEvent(ctx, model.WebhookEventTrackerCreated, &tracker)
|
||||
webhooks.DeliverUserTrackerEvent(ctx, model.WebhookEventTrackerCreated, &tracker)
|
||||
return &tracker, nil
|
||||
}
|
||||
|
||||
|
@ -243,6 +244,7 @@ func (r *mutationResolver) UpdateTracker(ctx context.Context, id int, input map[
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyTrackerEvent(ctx, &tracker, "tracker:update")
|
||||
webhooks.DeliverUserTrackerEvent(ctx, model.WebhookEventTrackerUpdate, &tracker)
|
||||
webhooks.DeliverTrackerEvent(ctx, model.WebhookEventTrackerUpdate, &tracker)
|
||||
return &tracker, nil
|
||||
}
|
||||
|
@ -266,6 +268,10 @@ func (r *mutationResolver) DeleteTracker(ctx context.Context, id int) (*model.Tr
|
|||
return err
|
||||
}
|
||||
tracker.Access = model.ACCESS_ALL
|
||||
|
||||
webhooks.DeliverLegacyTrackerDelete(ctx, tracker.ID, user.UserID)
|
||||
webhooks.DeliverUserTrackerEvent(ctx, model.WebhookEventTrackerDeleted, &tracker)
|
||||
webhooks.DeliverTrackerEvent(ctx, model.WebhookEventTrackerDeleted, &tracker)
|
||||
return nil
|
||||
}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
|
@ -273,9 +279,6 @@ func (r *mutationResolver) DeleteTracker(ctx context.Context, id int) (*model.Tr
|
|||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webhooks.DeliverLegacyTrackerDelete(ctx, tracker.ID, user.UserID)
|
||||
webhooks.DeliverTrackerEvent(ctx, model.WebhookEventTrackerDeleted, &tracker)
|
||||
return &tracker, nil
|
||||
}
|
||||
|
||||
|
@ -341,6 +344,7 @@ func (r *mutationResolver) UpdateTrackerACL(ctx context.Context, trackerID int,
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyTrackerEvent(ctx, &tracker, "tracker:update")
|
||||
webhooks.DeliverUserTrackerEvent(ctx, model.WebhookEventTrackerUpdate, &tracker)
|
||||
webhooks.DeliverTrackerEvent(ctx, model.WebhookEventTrackerUpdate, &tracker)
|
||||
acl := &model.DefaultACL{}
|
||||
acl.SetBits(bits)
|
||||
|
@ -572,6 +576,7 @@ func (r *mutationResolver) CreateLabel(ctx context.Context, trackerID int, name
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyLabelCreate(ctx, tracker, &label)
|
||||
webhooks.DeliverTrackerLabelEvent(ctx, model.WebhookEventLabelCreated, label.TrackerID, &label)
|
||||
return &label, nil
|
||||
}
|
||||
|
||||
|
@ -608,31 +613,28 @@ func (r *mutationResolver) UpdateLabel(ctx context.Context, id int, input map[st
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
label, err := loaders.ForContext(ctx).LabelsByID.Load(id)
|
||||
if err != nil || label == nil {
|
||||
return nil, err
|
||||
}
|
||||
tracker, err := loaders.ForContext(ctx).TrackersByID.Load(label.TrackerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tracker.OwnerID != auth.ForContext(ctx).UserID {
|
||||
return nil, fmt.Errorf("Access denied")
|
||||
}
|
||||
|
||||
var label model.Label
|
||||
userID := auth.ForContext(ctx).UserID
|
||||
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
|
||||
var err error
|
||||
if len(input) != 0 {
|
||||
_, err = query.
|
||||
Where(database.WithAlias(label.Alias(), `id`)+"= ?", id).
|
||||
RunWith(tx).
|
||||
ExecContext(ctx)
|
||||
row := query.
|
||||
From(`tracker tr`).
|
||||
Where(`label.id = ? AND tracker_id = tr.id AND tr.owner_id = ?`, id, userID).
|
||||
Suffix(`RETURNING label.id, label.tracker_id, label.created, label.name, label.color, label.text_color`).
|
||||
RunWith(tx).
|
||||
QueryRowContext(ctx)
|
||||
if err := row.Scan(&label.ID, &label.TrackerID, &label.Created,
|
||||
&label.Name, &label.BackgroundColor, &label.ForegroundColor); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("No label by ID %d found for this user", id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return label, nil
|
||||
webhooks.DeliverTrackerLabelEvent(ctx, model.WebhookEventLabelUpdate, label.TrackerID, &label)
|
||||
return &label, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteLabel(ctx context.Context, id int) (*model.Label, error) {
|
||||
|
@ -657,6 +659,7 @@ func (r *mutationResolver) DeleteLabel(ctx context.Context, id int) (*model.Labe
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyLabelDelete(ctx, label.TrackerID, label.ID)
|
||||
webhooks.DeliverTrackerLabelEvent(ctx, model.WebhookEventLabelDeleted, label.TrackerID, &label)
|
||||
return &label, nil
|
||||
}
|
||||
|
||||
|
@ -820,7 +823,8 @@ func (r *mutationResolver) SubmitTicket(ctx context.Context, trackerID int, inpu
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyTicketCreate(ctx, tracker, &ticket)
|
||||
webhooks.DeliverTicketEvent(ctx, model.WebhookEventTicketCreated, &ticket)
|
||||
webhooks.DeliverUserTicketEvent(ctx, model.WebhookEventTicketCreated, &ticket)
|
||||
webhooks.DeliverTrackerTicketEvent(ctx, model.WebhookEventTicketCreated, ticket.TrackerID, &ticket)
|
||||
return &ticket, nil
|
||||
}
|
||||
|
||||
|
@ -930,7 +934,8 @@ func (r *mutationResolver) SubmitEmail(ctx context.Context, trackerID int, input
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyTicketCreate(ctx, tracker, &ticket)
|
||||
webhooks.DeliverTicketEvent(ctx, model.WebhookEventTicketCreated, &ticket)
|
||||
webhooks.DeliverUserTicketEvent(ctx, model.WebhookEventTicketCreated, &ticket)
|
||||
webhooks.DeliverTrackerTicketEvent(ctx, model.WebhookEventTicketCreated, ticket.TrackerID, &ticket)
|
||||
return &ticket, nil
|
||||
}
|
||||
|
||||
|
@ -1001,6 +1006,7 @@ func (r *mutationResolver) UpdateTicket(ctx context.Context, trackerID int, tick
|
|||
return nil, err
|
||||
}
|
||||
|
||||
webhooks.DeliverTrackerTicketEvent(ctx, model.WebhookEventTicketUpdate, ticket.TrackerID, ticket)
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
|
@ -1072,11 +1078,12 @@ func (r *mutationResolver) UpdateTicketStatus(ctx context.Context, trackerID int
|
|||
columns := database.Columns(ctx, &event)
|
||||
|
||||
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
|
||||
_, err := update.
|
||||
row := update.
|
||||
Where(`ticket.id = ?`, ticket.PKID).
|
||||
Suffix(`RETURNING ticket.status, ticket.resolution`).
|
||||
RunWith(tx).
|
||||
ExecContext(ctx)
|
||||
if err != nil {
|
||||
QueryRowContext(ctx)
|
||||
if err := row.Scan(&ticket.RawStatus, &ticket.RawResolution); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1111,6 +1118,8 @@ func (r *mutationResolver) UpdateTicketStatus(ctx context.Context, trackerID int
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyEventCreate(ctx, tracker, ticket, &event)
|
||||
webhooks.DeliverTrackerTicketEvent(ctx, model.WebhookEventTicketUpdate, ticket.TrackerID, ticket)
|
||||
webhooks.DeliverTrackerEventCreated(ctx, ticket.TrackerID, &event)
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
|
@ -1266,6 +1275,7 @@ func (r *mutationResolver) SubmitComment(ctx context.Context, trackerID int, tic
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyEventCreate(ctx, tracker, ticket, &event)
|
||||
webhooks.DeliverTrackerEventCreated(ctx, ticket.TrackerID, &event)
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
|
@ -1392,6 +1402,7 @@ func (r *mutationResolver) AssignUser(ctx context.Context, trackerID int, ticket
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyEventCreate(ctx, tracker, ticket, &event)
|
||||
webhooks.DeliverTrackerEventCreated(ctx, ticket.TrackerID, &event)
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
|
@ -1515,6 +1526,7 @@ func (r *mutationResolver) UnassignUser(ctx context.Context, trackerID int, tick
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyEventCreate(ctx, tracker, ticket, &event)
|
||||
webhooks.DeliverTrackerEventCreated(ctx, ticket.TrackerID, &event)
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
|
@ -1602,6 +1614,7 @@ func (r *mutationResolver) LabelTicket(ctx context.Context, trackerID int, ticke
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyEventCreate(ctx, tracker, ticket, &event)
|
||||
webhooks.DeliverTrackerEventCreated(ctx, ticket.TrackerID, &event)
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
|
@ -1682,10 +1695,11 @@ func (r *mutationResolver) UnlabelTicket(ctx context.Context, trackerID int, tic
|
|||
return nil, err
|
||||
}
|
||||
webhooks.DeliverLegacyEventCreate(ctx, tracker, ticket, &event)
|
||||
webhooks.DeliverTrackerEventCreated(ctx, ticket.TrackerID, &event)
|
||||
return &event, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateWebhook(ctx context.Context, config model.UserWebhookInput) (model.WebhookSubscription, error) {
|
||||
func (r *mutationResolver) CreateUserWebhook(ctx context.Context, config model.UserWebhookInput) (model.WebhookSubscription, error) {
|
||||
schema := server.ForContext(ctx).Schema
|
||||
if err := corewebhooks.Validate(schema, config.Query); err != nil {
|
||||
return nil, err
|
||||
|
@ -1714,6 +1728,8 @@ func (r *mutationResolver) CreateWebhook(ctx context.Context, config model.UserW
|
|||
access = "TRACKERS"
|
||||
case model.WebhookEventTicketCreated:
|
||||
access = "TICKETS"
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported event %s", ev.String())
|
||||
}
|
||||
if !user.Grants.Has(access, auth.RO) {
|
||||
return nil, fmt.Errorf("Insufficient access granted for webhook event %s", ev.String())
|
||||
|
@ -1759,7 +1775,7 @@ func (r *mutationResolver) CreateWebhook(ctx context.Context, config model.UserW
|
|||
return &sub, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteWebhook(ctx context.Context, id int) (model.WebhookSubscription, error) {
|
||||
func (r *mutationResolver) DeleteUserWebhook(ctx context.Context, id int) (model.WebhookSubscription, error) {
|
||||
var sub model.UserWebhookSubscription
|
||||
|
||||
filter, err := corewebhooks.FilterWebhooks(ctx)
|
||||
|
@ -1789,6 +1805,117 @@ func (r *mutationResolver) DeleteWebhook(ctx context.Context, id int) (model.Web
|
|||
return &sub, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) CreateTrackerWebhook(ctx context.Context, trackerID int, config model.TrackerWebhookInput) (model.WebhookSubscription, error) {
|
||||
schema := server.ForContext(ctx).Schema
|
||||
if err := corewebhooks.Validate(schema, config.Query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := auth.ForContext(ctx)
|
||||
ac, err := corewebhooks.NewAuthConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sub model.TrackerWebhookSubscription
|
||||
if len(config.Events) == 0 {
|
||||
return nil, fmt.Errorf("Must specify at least one event")
|
||||
}
|
||||
events := make([]string, len(config.Events))
|
||||
for i, ev := range config.Events {
|
||||
events[i] = ev.String()
|
||||
// TODO: gqlgen does not support doing anything useful with directives
|
||||
// on enums at the time of writing, so we have to do a little bit of
|
||||
// manual fuckery
|
||||
var access string
|
||||
switch ev {
|
||||
case model.WebhookEventTrackerUpdate, model.WebhookEventTrackerDeleted,
|
||||
model.WebhookEventLabelCreated, model.WebhookEventLabelUpdate,
|
||||
model.WebhookEventLabelDeleted:
|
||||
access = "TRACKERS"
|
||||
case model.WebhookEventTicketCreated, model.WebhookEventTicketUpdate:
|
||||
access = "TICKETS"
|
||||
case model.WebhookEventEventCreated:
|
||||
access = "EVENTS"
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported event %s", ev.String())
|
||||
}
|
||||
if !user.Grants.Has(access, auth.RO) {
|
||||
return nil, fmt.Errorf("Insufficient access granted for webhook event %s", ev.String())
|
||||
}
|
||||
}
|
||||
|
||||
u, err := url.Parse(config.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if u.Host == "" {
|
||||
return nil, fmt.Errorf("Cannot use URL without host")
|
||||
} else if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return nil, fmt.Errorf("Cannot use non-HTTP or HTTPS URL")
|
||||
}
|
||||
|
||||
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
|
||||
row := tx.QueryRowContext(ctx, `
|
||||
INSERT INTO gql_tracker_wh_sub (
|
||||
created, events, url, query,
|
||||
auth_method,
|
||||
token_hash, grants, client_id, expires,
|
||||
node_id,
|
||||
user_id,
|
||||
tracker_id
|
||||
) VALUES (
|
||||
NOW() at time zone 'utc',
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
|
||||
) RETURNING id, url, query, events, user_id, tracker_id;`,
|
||||
pq.Array(events), config.URL, config.Query,
|
||||
ac.AuthMethod,
|
||||
ac.TokenHash, ac.Grants, ac.ClientID, ac.Expires, // OAUTH2
|
||||
ac.NodeID, // INTERNAL
|
||||
user.UserID,
|
||||
trackerID)
|
||||
|
||||
if err := row.Scan(&sub.ID, &sub.URL,
|
||||
&sub.Query, pq.Array(&sub.Events), &sub.UserID, &sub.TrackerID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteTrackerWebhook(ctx context.Context, id int) (model.WebhookSubscription, error) {
|
||||
var sub model.TrackerWebhookSubscription
|
||||
|
||||
filter, err := corewebhooks.FilterWebhooks(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
|
||||
row := sq.Delete(`gql_tracker_wh_sub`).
|
||||
PlaceholderFormat(sq.Dollar).
|
||||
Where(sq.And{sq.Expr(`id = ?`, id), filter}).
|
||||
Suffix(`RETURNING id, url, query, events, user_id, tracker_id`).
|
||||
RunWith(tx).
|
||||
QueryRowContext(ctx)
|
||||
if err := row.Scan(&sub.ID, &sub.URL,
|
||||
&sub.Query, pq.Array(&sub.Events), &sub.UserID, &sub.TrackerID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
|
||||
return &model.Version{
|
||||
Major: 0,
|
||||
|
@ -2258,6 +2385,71 @@ func (r *trackerResolver) Export(ctx context.Context, obj *model.Tracker) (strin
|
|||
panic(fmt.Errorf("not implemented")) // TODO
|
||||
}
|
||||
|
||||
func (r *trackerResolver) Webhooks(ctx context.Context, obj *model.Tracker, cursor *coremodel.Cursor) (*model.WebhookSubscriptionCursor, error) {
|
||||
if cursor == nil {
|
||||
cursor = coremodel.NewCursor(nil)
|
||||
}
|
||||
|
||||
filter, err := corewebhooks.FilterWebhooks(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var subs []model.WebhookSubscription
|
||||
if err := database.WithTx(ctx, &sql.TxOptions{
|
||||
Isolation: 0,
|
||||
ReadOnly: true,
|
||||
}, func(tx *sql.Tx) error {
|
||||
sub := (&model.TrackerWebhookSubscription{}).As(`sub`)
|
||||
query := database.
|
||||
Select(ctx, sub).
|
||||
From(`gql_tracker_wh_sub sub`).
|
||||
Where(sq.And{sq.Expr(`tracker_id = ?`, obj.ID), filter})
|
||||
subs, cursor = sub.QueryWithCursor(ctx, tx, query, cursor)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.WebhookSubscriptionCursor{subs, cursor}, nil
|
||||
}
|
||||
|
||||
func (r *trackerResolver) Webhook(ctx context.Context, obj *model.Tracker, id int) (model.WebhookSubscription, error) {
|
||||
var sub model.TrackerWebhookSubscription
|
||||
|
||||
filter, err := corewebhooks.FilterWebhooks(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := database.WithTx(ctx, &sql.TxOptions{
|
||||
Isolation: 0,
|
||||
ReadOnly: true,
|
||||
}, func(tx *sql.Tx) error {
|
||||
row := database.
|
||||
Select(ctx, &sub).
|
||||
From(`gql_tracker_wh_sub`).
|
||||
Where(sq.And{
|
||||
sq.Expr(`id = ?`, id),
|
||||
sq.Expr(`tracker_id = ?`, obj.ID),
|
||||
filter,
|
||||
}).
|
||||
RunWith(tx).
|
||||
QueryRowContext(ctx)
|
||||
if err := row.Scan(database.Scan(ctx, &sub)...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sub, nil
|
||||
}
|
||||
|
||||
func (r *trackerACLResolver) Tracker(ctx context.Context, obj *model.TrackerACL) (*model.Tracker, error) {
|
||||
return loaders.ForContext(ctx).TrackersByID.Load(obj.TrackerID)
|
||||
}
|
||||
|
@ -2270,6 +2462,161 @@ func (r *trackerSubscriptionResolver) Tracker(ctx context.Context, obj *model.Tr
|
|||
return loaders.ForContext(ctx).TrackersByID.Load(obj.TrackerID)
|
||||
}
|
||||
|
||||
func (r *trackerWebhookSubscriptionResolver) Client(ctx context.Context, obj *model.TrackerWebhookSubscription) (*model.OAuthClient, error) {
|
||||
if obj.ClientID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &model.OAuthClient{
|
||||
UUID: *obj.ClientID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *trackerWebhookSubscriptionResolver) Deliveries(ctx context.Context, obj *model.TrackerWebhookSubscription, cursor *coremodel.Cursor) (*model.WebhookDeliveryCursor, error) {
|
||||
if cursor == nil {
|
||||
cursor = coremodel.NewCursor(nil)
|
||||
}
|
||||
|
||||
var deliveries []*model.WebhookDelivery
|
||||
if err := database.WithTx(ctx, &sql.TxOptions{
|
||||
Isolation: 0,
|
||||
ReadOnly: true,
|
||||
}, func(tx *sql.Tx) error {
|
||||
d := (&model.WebhookDelivery{}).
|
||||
WithName(`tracker`).
|
||||
As(`delivery`)
|
||||
query := database.
|
||||
Select(ctx, d).
|
||||
From(`gql_tracker_wh_delivery delivery`).
|
||||
Where(`delivery.subscription_id = ?`, obj.ID)
|
||||
deliveries, cursor = d.QueryWithCursor(ctx, tx, query, cursor)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.WebhookDeliveryCursor{deliveries, cursor}, nil
|
||||
}
|
||||
|
||||
func (r *trackerWebhookSubscriptionResolver) Sample(ctx context.Context, obj *model.TrackerWebhookSubscription, event model.WebhookEvent) (string, error) {
|
||||
payloadUUID := uuid.New()
|
||||
webhook := corewebhooks.WebhookContext{
|
||||
User: auth.ForContext(ctx),
|
||||
PayloadUUID: payloadUUID,
|
||||
Name: "tracker",
|
||||
Event: event.String(),
|
||||
Subscription: &corewebhooks.WebhookSubscription{
|
||||
ID: obj.ID,
|
||||
URL: obj.URL,
|
||||
Query: obj.Query,
|
||||
AuthMethod: obj.AuthMethod,
|
||||
TokenHash: obj.TokenHash,
|
||||
Grants: obj.Grants,
|
||||
ClientID: obj.ClientID,
|
||||
Expires: obj.Expires,
|
||||
NodeID: obj.NodeID,
|
||||
},
|
||||
}
|
||||
|
||||
auth := auth.ForContext(ctx)
|
||||
switch event {
|
||||
case model.WebhookEventTrackerUpdate, model.WebhookEventTrackerDeleted:
|
||||
desc := "Sample todo tracker for testing webhooks"
|
||||
webhook.Payload = &model.TrackerEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
Tracker: &model.Tracker{
|
||||
ID: -1,
|
||||
Created: time.Now().UTC(),
|
||||
Updated: time.Now().UTC(),
|
||||
Name: "sample-tracker",
|
||||
Description: &desc,
|
||||
Visibility: model.VisibilityPublic,
|
||||
|
||||
OwnerID: auth.UserID,
|
||||
Access: model.ACCESS_ALL,
|
||||
DefaultAccess: model.ACCESS_ALL,
|
||||
ACLID: nil,
|
||||
},
|
||||
}
|
||||
case model.WebhookEventLabelCreated, model.WebhookEventLabelUpdate,
|
||||
model.WebhookEventLabelDeleted:
|
||||
webhook.Payload = &model.LabelEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
Label: &model.Label{
|
||||
ID: -1,
|
||||
Created: time.Now().UTC(),
|
||||
Name: "sample-label",
|
||||
BackgroundColor: "#ffffff",
|
||||
ForegroundColor: "#000000",
|
||||
TrackerID: -1,
|
||||
},
|
||||
}
|
||||
case model.WebhookEventTicketCreated, model.WebhookEventTicketUpdate:
|
||||
body := "This is a sample ticket body."
|
||||
webhook.Payload = &model.TicketEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
Ticket: &model.Ticket{
|
||||
ID: 1,
|
||||
Created: time.Now().UTC(),
|
||||
Updated: time.Now().UTC(),
|
||||
Subject: "A sample ticket",
|
||||
Body: &body,
|
||||
PKID: -1,
|
||||
TrackerID: -1,
|
||||
TrackerName: "sample-tracker",
|
||||
OwnerName: auth.Username,
|
||||
SubmitterID: -1,
|
||||
RawAuthenticity: model.AUTH_AUTHENTIC,
|
||||
RawStatus: model.STATUS_REPORTED,
|
||||
RawResolution: model.RESOLVED_UNRESOLVED,
|
||||
},
|
||||
}
|
||||
case model.WebhookEventEventCreated:
|
||||
oldStatus := model.STATUS_REPORTED
|
||||
newStatus := model.STATUS_RESOLVED
|
||||
oldResolution := model.RESOLVED_UNRESOLVED
|
||||
newResolution := model.RESOLVED_FIXED
|
||||
webhook.Payload = &model.EventCreated{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
NewEvent: &model.Event{
|
||||
ID: -1,
|
||||
Created: time.Now().UTC(),
|
||||
EventType: model.EVENT_STATUS_CHANGE,
|
||||
ParticipantID: -1,
|
||||
TicketID: -1,
|
||||
ByParticipantID: nil,
|
||||
CommentID: nil,
|
||||
LabelID: nil,
|
||||
FromTicketID: nil,
|
||||
OldStatus: &oldStatus,
|
||||
NewStatus: &newStatus,
|
||||
OldResolution: &oldResolution,
|
||||
NewResolution: &newResolution,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("Unsupported event %s", event.String())
|
||||
}
|
||||
|
||||
subctx := corewebhooks.Context(ctx, webhook.Payload)
|
||||
bytes, err := webhook.Exec(subctx, server.ForContext(ctx).Schema)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func (r *trackerWebhookSubscriptionResolver) Tracker(ctx context.Context, obj *model.TrackerWebhookSubscription) (*model.Tracker, error) {
|
||||
return loaders.ForContext(ctx).TrackersByID.Load(obj.TrackerID)
|
||||
}
|
||||
|
||||
func (r *userResolver) Trackers(ctx context.Context, obj *model.User, cursor *coremodel.Cursor) (*model.TrackerCursor, error) {
|
||||
if cursor == nil {
|
||||
cursor = coremodel.NewCursor(nil)
|
||||
|
@ -2370,12 +2717,23 @@ func (r *webhookDeliveryResolver) Subscription(ctx context.Context, obj *model.W
|
|||
ReadOnly: true,
|
||||
}, func(tx *sql.Tx) error {
|
||||
// XXX: This needs some work to generalize to other kinds of webhooks
|
||||
subscription := (&model.UserWebhookSubscription{}).As(`sub`)
|
||||
var subscription interface {
|
||||
model.WebhookSubscription
|
||||
database.Model
|
||||
} = nil
|
||||
switch obj.Name {
|
||||
case "user":
|
||||
subscription = (&model.UserWebhookSubscription{}).As(`sub`)
|
||||
case "tracker":
|
||||
subscription = (&model.TrackerWebhookSubscription{}).As(`sub`)
|
||||
default:
|
||||
panic(fmt.Errorf("unknown webhook name %q", obj.Name))
|
||||
}
|
||||
// Note: No filter needed because, if we have access to the delivery,
|
||||
// we also have access to the subscription.
|
||||
row := database.
|
||||
Select(ctx, subscription).
|
||||
From(`gql_user_wh_sub sub`).
|
||||
From(`gql_`+obj.Name+`_wh_sub sub`).
|
||||
Where(`sub.id = ?`, obj.SubscriptionID).
|
||||
RunWith(tx).
|
||||
QueryRowContext(ctx)
|
||||
|
@ -2439,6 +2797,11 @@ func (r *Resolver) TrackerSubscription() api.TrackerSubscriptionResolver {
|
|||
return &trackerSubscriptionResolver{r}
|
||||
}
|
||||
|
||||
// TrackerWebhookSubscription returns api.TrackerWebhookSubscriptionResolver implementation.
|
||||
func (r *Resolver) TrackerWebhookSubscription() api.TrackerWebhookSubscriptionResolver {
|
||||
return &trackerWebhookSubscriptionResolver{r}
|
||||
}
|
||||
|
||||
// User returns api.UserResolver implementation.
|
||||
func (r *Resolver) User() api.UserResolver { return &userResolver{r} }
|
||||
|
||||
|
@ -2468,6 +2831,7 @@ type ticketSubscriptionResolver struct{ *Resolver }
|
|||
type trackerResolver struct{ *Resolver }
|
||||
type trackerACLResolver struct{ *Resolver }
|
||||
type trackerSubscriptionResolver struct{ *Resolver }
|
||||
type trackerWebhookSubscriptionResolver struct{ *Resolver }
|
||||
type userResolver struct{ *Resolver }
|
||||
type userMentionResolver struct{ *Resolver }
|
||||
type userWebhookSubscriptionResolver struct{ *Resolver }
|
||||
|
|
|
@ -24,7 +24,19 @@ func deliverUserWebhook(ctx context.Context, event model.WebhookEvent,
|
|||
payloadUUID, payload)
|
||||
}
|
||||
|
||||
func DeliverTrackerEvent(ctx context.Context,
|
||||
func deliverTrackerWebhook(ctx context.Context, trackerID int,
|
||||
event model.WebhookEvent, payload model.WebhookPayload, payloadUUID uuid.UUID) {
|
||||
q := webhooks.ForContext(ctx)
|
||||
userID := auth.ForContext(ctx).UserID
|
||||
query := sq.
|
||||
Select().
|
||||
From("gql_tracker_wh_sub sub").
|
||||
Where("sub.user_id = ? AND sub.tracker_id = ?", userID, trackerID)
|
||||
q.Schedule(ctx, query, "tracker", event.String(),
|
||||
payloadUUID, payload)
|
||||
}
|
||||
|
||||
func DeliverUserTrackerEvent(ctx context.Context,
|
||||
event model.WebhookEvent, tracker *model.Tracker) {
|
||||
payloadUUID := uuid.New()
|
||||
payload := model.TrackerEvent{
|
||||
|
@ -36,14 +48,62 @@ func DeliverTrackerEvent(ctx context.Context,
|
|||
deliverUserWebhook(ctx, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTicketEvent(ctx context.Context,
|
||||
func DeliverUserTicketEvent(ctx context.Context,
|
||||
event model.WebhookEvent, ticket *model.Ticket) {
|
||||
payloadUUID := uuid.New()
|
||||
payload := model.TicketEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
Ticket: ticket,
|
||||
}
|
||||
deliverUserWebhook(ctx, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTrackerEvent(ctx context.Context,
|
||||
event model.WebhookEvent, tracker *model.Tracker) {
|
||||
payloadUUID := uuid.New()
|
||||
payload := model.TrackerEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
Tracker: tracker,
|
||||
}
|
||||
deliverTrackerWebhook(ctx, tracker.ID, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTrackerLabelEvent(ctx context.Context,
|
||||
event model.WebhookEvent, trackerID int, label *model.Label) {
|
||||
payloadUUID := uuid.New()
|
||||
payload := model.LabelEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
Label: label,
|
||||
}
|
||||
deliverTrackerWebhook(ctx, trackerID, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTrackerTicketEvent(ctx context.Context,
|
||||
event model.WebhookEvent, trackerID int, ticket *model.Ticket) {
|
||||
payloadUUID := uuid.New()
|
||||
payload := model.TicketEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
Ticket: ticket,
|
||||
}
|
||||
deliverTrackerWebhook(ctx, trackerID, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTrackerEventCreated(ctx context.Context, trackerID int, newEvent *model.Event) {
|
||||
event := model.WebhookEventEventCreated
|
||||
payloadUUID := uuid.New()
|
||||
payload := model.EventCreated{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
NewEvent: newEvent,
|
||||
}
|
||||
deliverTrackerWebhook(ctx, trackerID, event, &payload, payloadUUID)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
"""Add GraphQL tracker webhook tables
|
||||
|
||||
Revision ID: 87daab81985b
|
||||
Revises: dbed5c6ea613
|
||||
Create Date: 2022-04-11 14:21:35.885142
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '87daab81985b'
|
||||
down_revision = 'dbed5c6ea613'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute("""
|
||||
CREATE TYPE tracker_webhook_event AS ENUM (
|
||||
'TRACKER_UPDATE',
|
||||
'TRACKER_DELETED',
|
||||
'TICKET_CREATED',
|
||||
'TICKET_UPDATE',
|
||||
'LABEL_CREATED',
|
||||
'LABEL_UPDATE',
|
||||
'LABEL_DELETED',
|
||||
'EVENT_CREATED'
|
||||
);
|
||||
|
||||
CREATE TABLE gql_tracker_wh_sub (
|
||||
id serial PRIMARY KEY,
|
||||
created timestamp NOT NULL,
|
||||
events tracker_webhook_event[] NOT NULL check (array_length(events, 1) > 0),
|
||||
url varchar NOT NULL,
|
||||
query varchar NOT NULL,
|
||||
|
||||
auth_method auth_method NOT NULL check (auth_method in ('OAUTH2', 'INTERNAL')),
|
||||
token_hash varchar(128) check ((auth_method = 'OAUTH2') = (token_hash IS NOT NULL)),
|
||||
grants varchar,
|
||||
client_id uuid,
|
||||
expires timestamp check ((auth_method = 'OAUTH2') = (expires IS NOT NULL)),
|
||||
node_id varchar check ((auth_method = 'INTERNAL') = (node_id IS NOT NULL)),
|
||||
|
||||
user_id integer NOT NULL references "user"(id),
|
||||
tracker_id integer NOT NULL references "tracker"(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX gql_tracker_wh_sub_token_hash_idx ON gql_tracker_wh_sub (token_hash);
|
||||
|
||||
CREATE TABLE gql_tracker_wh_delivery (
|
||||
id serial PRIMARY KEY,
|
||||
uuid uuid NOT NULL,
|
||||
date timestamp NOT NULL,
|
||||
event tracker_webhook_event NOT NULL,
|
||||
subscription_id integer NOT NULL references gql_tracker_wh_sub(id) ON DELETE CASCADE,
|
||||
request_body varchar NOT NULL,
|
||||
response_body varchar,
|
||||
response_headers varchar,
|
||||
response_status integer
|
||||
);
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("""
|
||||
DROP TABLE gql_tracker_wh_delivery;
|
||||
DROP INDEX gql_tracker_wh_sub_token_hash_idx;
|
||||
DROP TABLE gql_tracker_wh_sub;
|
||||
DROP TYPE tracker_webhook_event;
|
||||
""")
|
Loading…
Reference in New Issue