Skip to content

Commit

Permalink
Add Event Webhook Support with DocRootChanged Event (#1156)
Browse files Browse the repository at this point in the history
Implements event webhook functionality with configurable URLs and event types
at the project level. Introduces `DocRootChanged` event to enable accurate
document update tracking, while updating database schema, admin service,
and CLI to support webhook configuration.
  • Loading branch information
window9u authored Feb 20, 2025
1 parent 8d1f1ae commit 5299469
Show file tree
Hide file tree
Showing 35 changed files with 1,480 additions and 369 deletions.
8 changes: 8 additions & 0 deletions api/converter/from_pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func FromProject(pbProject *api.Project) *types.Project {
Name: pbProject.Name,
AuthWebhookURL: pbProject.AuthWebhookUrl,
AuthWebhookMethods: pbProject.AuthWebhookMethods,
EventWebhookURL: pbProject.EventWebhookUrl,
EventWebhookEvents: pbProject.EventWebhookEvents,
ClientDeactivateThreshold: pbProject.ClientDeactivateThreshold,
PublicKey: pbProject.PublicKey,
SecretKey: pbProject.SecretKey,
Expand Down Expand Up @@ -922,6 +924,12 @@ func FromUpdatableProjectFields(pbProjectFields *api.UpdatableProjectFields) (*t
if pbProjectFields.AuthWebhookMethods != nil {
updatableProjectFields.AuthWebhookMethods = &pbProjectFields.AuthWebhookMethods.Methods
}
if pbProjectFields.EventWebhookUrl != nil {
updatableProjectFields.EventWebhookURL = &pbProjectFields.EventWebhookUrl.Value
}
if pbProjectFields.EventWebhookEvents != nil {
updatableProjectFields.EventWebhookEvents = &pbProjectFields.EventWebhookEvents.Events
}
if pbProjectFields.ClientDeactivateThreshold != nil {
updatableProjectFields.ClientDeactivateThreshold = &pbProjectFields.ClientDeactivateThreshold.Value
}
Expand Down
12 changes: 12 additions & 0 deletions api/converter/to_pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ func ToProject(project *types.Project) *api.Project {
Name: project.Name,
AuthWebhookUrl: project.AuthWebhookURL,
AuthWebhookMethods: project.AuthWebhookMethods,
EventWebhookUrl: project.EventWebhookURL,
EventWebhookEvents: project.EventWebhookEvents,
ClientDeactivateThreshold: project.ClientDeactivateThreshold,
PublicKey: project.PublicKey,
SecretKey: project.SecretKey,
Expand Down Expand Up @@ -562,6 +564,16 @@ func ToUpdatableProjectFields(fields *types.UpdatableProjectFields) (*api.Updata
} else {
pbUpdatableProjectFields.AuthWebhookMethods = nil
}
if fields.EventWebhookURL != nil {
pbUpdatableProjectFields.EventWebhookUrl = &wrapperspb.StringValue{Value: *fields.EventWebhookURL}
}
if fields.EventWebhookEvents != nil {
pbUpdatableProjectFields.EventWebhookEvents = &api.UpdatableProjectFields_EventWebhookEvents{
Events: *fields.EventWebhookEvents,
}
} else {
pbUpdatableProjectFields.EventWebhookEvents = nil
}
if fields.ClientDeactivateThreshold != nil {
pbUpdatableProjectFields.ClientDeactivateThreshold = &wrapperspb.StringValue{
Value: *fields.ClientDeactivateThreshold,
Expand Down
37 changes: 37 additions & 0 deletions api/docs/yorkie/v1/admin.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,18 @@ components:
description: ""
title: created_at
type: object
eventWebhookEvents:
additionalProperties: false
description: ""
items:
type: string
title: event_webhook_events
type: array
eventWebhookUrl:
additionalProperties: false
description: ""
title: event_webhook_url
type: string
id:
additionalProperties: false
description: ""
Expand Down Expand Up @@ -2132,6 +2144,18 @@ components:
description: ""
title: client_deactivate_threshold
type: object
eventWebhookEvents:
$ref: '#/components/schemas/yorkie.v1.UpdatableProjectFields.EventWebhookEvents'
additionalProperties: false
description: ""
title: event_webhook_events
type: object
eventWebhookUrl:
$ref: '#/components/schemas/google.protobuf.StringValue'
additionalProperties: false
description: ""
title: event_webhook_url
type: object
name:
$ref: '#/components/schemas/google.protobuf.StringValue'
additionalProperties: false
Expand All @@ -2153,6 +2177,19 @@ components:
type: array
title: AuthWebhookMethods
type: object
yorkie.v1.UpdatableProjectFields.EventWebhookEvents:
additionalProperties: false
description: ""
properties:
events:
additionalProperties: false
description: ""
items:
type: string
title: events
type: array
title: EventWebhookEvents
type: object
yorkie.v1.UpdateProjectRequest:
additionalProperties: false
description: ""
Expand Down
12 changes: 12 additions & 0 deletions api/docs/yorkie/v1/cluster.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,18 @@ components:
description: ""
title: created_at
type: object
eventWebhookEvents:
additionalProperties: false
description: ""
items:
type: string
title: event_webhook_events
type: array
eventWebhookUrl:
additionalProperties: false
description: ""
title: event_webhook_url
type: string
id:
additionalProperties: false
description: ""
Expand Down
37 changes: 37 additions & 0 deletions api/docs/yorkie/v1/resources.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1335,6 +1335,18 @@ components:
description: ""
title: created_at
type: object
eventWebhookEvents:
additionalProperties: false
description: ""
items:
type: string
title: event_webhook_events
type: array
eventWebhookUrl:
additionalProperties: false
description: ""
title: event_webhook_url
type: string
id:
additionalProperties: false
description: ""
Expand Down Expand Up @@ -1687,6 +1699,18 @@ components:
description: ""
title: client_deactivate_threshold
type: object
eventWebhookEvents:
$ref: '#/components/schemas/yorkie.v1.UpdatableProjectFields.EventWebhookEvents'
additionalProperties: false
description: ""
title: event_webhook_events
type: object
eventWebhookUrl:
$ref: '#/components/schemas/google.protobuf.StringValue'
additionalProperties: false
description: ""
title: event_webhook_url
type: object
name:
$ref: '#/components/schemas/google.protobuf.StringValue'
additionalProperties: false
Expand All @@ -1708,6 +1732,19 @@ components:
type: array
title: AuthWebhookMethods
type: object
yorkie.v1.UpdatableProjectFields.EventWebhookEvents:
additionalProperties: false
description: ""
properties:
events:
additionalProperties: false
description: ""
items:
type: string
title: events
type: array
title: EventWebhookEvents
type: object
yorkie.v1.User:
additionalProperties: false
description: ""
Expand Down
42 changes: 42 additions & 0 deletions api/types/event_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2025 The Yorkie Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package types

// EventWebhookType represents event webhook type
type EventWebhookType string

const (
// DocRootChanged is an event that indicates the document's content was modified.
DocRootChanged EventWebhookType = "DocumentRootChanged"
)

// IsValidEventType checks whether the given event type is valid.
func IsValidEventType(eventType string) bool {
return eventType == string(DocRootChanged)
}

// EventWebhookAttribute represents the attribute of the webhook.
type EventWebhookAttribute struct {
Key string `json:"key"`
IssuedAt string `json:"issuedAt"`
}

// EventWebhookRequest represents the request of the webhook.
type EventWebhookRequest struct {
Type EventWebhookType `json:"type"`
Attributes EventWebhookAttribute `json:"attributes"`
}
14 changes: 14 additions & 0 deletions api/types/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const (
// modified by a change.
DocChangedEvent DocEventType = "document-changed"

// DocRootChangedEvent is an event indicating that document's root content
// is being changed by operation.
DocRootChangedEvent DocEventType = "document-root-changed"

// DocWatchedEvent is an event that occurs when document is watched
// by other clients.
DocWatchedEvent DocEventType = "document-watched"
Expand All @@ -43,6 +47,16 @@ const (
DocBroadcastEvent DocEventType = "document-broadcast"
)

// WebhookType returns a matched event webhook type.
func (t DocEventType) WebhookType() types.EventWebhookType {
switch t {
case DocRootChangedEvent:
return types.DocRootChanged
default:
return ""
}
}

// DocEventBody includes additional data specific to the DocEvent.
type DocEventBody struct {
Topic string
Expand Down
25 changes: 25 additions & 0 deletions api/types/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ type Project struct {
// AuthWebhookMethods is the methods that run the authorization webhook.
AuthWebhookMethods []string `json:"auth_webhook_methods"`

// EventWebhookURL is the url of the event webhook.
EventWebhookURL string `json:"event_webhook_url"`

// EventWebhookEvents are the events that event webhook will be triggered.
EventWebhookEvents []string `json:"event_webhook_events"`

// ClientDeactivateThreshold is the time after which clients in
// specific project are considered deactivate for housekeeping.
ClientDeactivateThreshold string `bson:"client_deactivate_threshold"`
Expand Down Expand Up @@ -73,3 +79,22 @@ func (p *Project) RequireAuth(method Method) bool {

return false
}

// RequireEventWebhook returns whether the given type requires to send event webhook.
func (p *Project) RequireEventWebhook(eventType EventWebhookType) bool {
if len(p.EventWebhookURL) == 0 {
return false
}

if len(p.EventWebhookEvents) == 0 {
return false
}

for _, t := range p.EventWebhookEvents {
if EventWebhookType(t) == eventType {
return true
}
}

return false
}
23 changes: 23 additions & 0 deletions api/types/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,27 @@ func TestProjectInfo(t *testing.T) {
}
assert.False(t, info3.RequireAuth(types.ActivateClient))
})

t.Run("require event webhook test", func(t *testing.T) {
// 1. Specify which event types to allow
validWebhookURL := "ValidWebhookURL"
info := &types.Project{
EventWebhookURL: validWebhookURL,
EventWebhookEvents: []string{string(types.DocRootChanged)},
}
assert.True(t, info.RequireEventWebhook(types.DocRootChanged))

// 2. No event types specified
info2 := &types.Project{
EventWebhookURL: validWebhookURL,
EventWebhookEvents: []string{},
}
assert.False(t, info2.RequireEventWebhook(types.DocRootChanged))

// 3. Empty webhook URL
info3 := &types.Project{
EventWebhookURL: "",
}
assert.False(t, info3.RequireEventWebhook(types.DocRootChanged))
})
}
35 changes: 34 additions & 1 deletion api/types/updatable_project_fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,24 @@ type UpdatableProjectFields struct {
// AuthWebhookMethods is the methods that run the authorization webhook.
AuthWebhookMethods *[]string `bson:"auth_webhook_methods,omitempty" validate:"omitempty,invalid_webhook_method"`

// EventWebhookURL is the URL of the event webhook.
EventWebhookURL *string `bson:"event_webhook_url,omitempty" validate:"omitempty,url|emptystring"`

// EventWebhookEvents is the events that trigger the webhook.
EventWebhookEvents *[]string `bson:"event_webhook_events,omitempty" validate:"omitempty,invalid_webhook_event"`

// ClientDeactivateThreshold is the time after which clients in specific project are considered deactivate.
ClientDeactivateThreshold *string `bson:"client_deactivate_threshold,omitempty" validate:"omitempty,min=2,duration"`
}

// Validate validates the UpdatableProjectFields.
func (i *UpdatableProjectFields) Validate() error {
if i.Name == nil && i.AuthWebhookURL == nil && i.AuthWebhookMethods == nil && i.ClientDeactivateThreshold == nil {
if i.Name == nil &&
i.AuthWebhookURL == nil &&
i.AuthWebhookMethods == nil &&
i.ClientDeactivateThreshold == nil &&
i.EventWebhookURL == nil &&
i.EventWebhookEvents == nil {
return ErrEmptyProjectFields
}

Expand All @@ -68,8 +79,30 @@ func init() {
fmt.Fprintln(os.Stderr, "updatable project fields: ", err)
os.Exit(1)
}

if err := validation.RegisterTranslation("invalid_webhook_method", "given {0} is invalid method"); err != nil {
fmt.Fprintln(os.Stderr, "updatable project fields: ", err)
os.Exit(1)
}

if err := validation.RegisterValidation(
"invalid_webhook_event",
func(level validation.FieldLevel) bool {
eventTypes := level.Field().Interface().([]string)
for _, eventType := range eventTypes {
if !IsValidEventType(eventType) {
return false
}
}
return true
},
); err != nil {
fmt.Fprintln(os.Stderr, "updatable project fields: ", err)
os.Exit(1)
}

if err := validation.RegisterTranslation("invalid_webhook_event", "given {0} is invalid event type"); err != nil {
fmt.Fprintln(os.Stderr, "updatable project fields: ", err)
os.Exit(1)
}
}
Loading

0 comments on commit 5299469

Please sign in to comment.