From 52994699c05e65aeb502f1ffbcb1cbebb1815bda Mon Sep 17 00:00:00 2001 From: Changyu Moon <121847433+window9u@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:36:06 +0900 Subject: [PATCH] Add Event Webhook Support with DocRootChanged Event (#1156) 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. --- api/converter/from_pb.go | 8 + api/converter/to_pb.go | 12 + api/docs/yorkie/v1/admin.openapi.yaml | 37 ++ api/docs/yorkie/v1/cluster.openapi.yaml | 12 + api/docs/yorkie/v1/resources.openapi.yaml | 37 ++ api/types/event_webhook.go | 42 ++ api/types/events/events.go | 14 + api/types/project.go | 25 + api/types/project_test.go | 23 + api/types/updatable_project_fields.go | 35 +- api/types/updatable_project_fields_test.go | 12 + api/yorkie/v1/resources.pb.go | 602 +++++++++++------- api/yorkie/v1/resources.proto | 16 +- cmd/yorkie/project/list.go | 4 + cmd/yorkie/project/update.go | 106 ++- cmd/yorkie/server.go | 37 +- pkg/webhook/client.go | 7 +- server/backend/backend.go | 29 +- server/backend/config.go | 70 +- server/backend/config_test.go | 27 +- server/backend/database/project_info.go | 16 + server/backend/database/project_info_test.go | 8 + .../backend/database/testcases/testcases.go | 47 +- server/config.go | 26 +- server/config.sample.yml | 12 + server/config_test.go | 13 + server/packs/packs.go | 17 + server/packs/packs_test.go | 29 +- server/rpc/auth/webhook.go | 6 +- server/rpc/server_test.go | 29 +- server/webhook/events.go | 92 +++ test/bench/grpc_bench_test.go | 4 + test/complex/main_test.go | 31 +- test/helper/helper.go | 64 +- test/integration/event_webhook_test.go | 300 +++++++++ 35 files changed, 1480 insertions(+), 369 deletions(-) create mode 100644 api/types/event_webhook.go create mode 100644 server/webhook/events.go create mode 100644 test/integration/event_webhook_test.go diff --git a/api/converter/from_pb.go b/api/converter/from_pb.go index aaaa9fce9..5ae2d48df 100644 --- a/api/converter/from_pb.go +++ b/api/converter/from_pb.go @@ -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, @@ -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 } diff --git a/api/converter/to_pb.go b/api/converter/to_pb.go index 9b4a2c8c5..7df242c8d 100644 --- a/api/converter/to_pb.go +++ b/api/converter/to_pb.go @@ -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, @@ -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, diff --git a/api/docs/yorkie/v1/admin.openapi.yaml b/api/docs/yorkie/v1/admin.openapi.yaml index d3f6c67b6..0ff205363 100644 --- a/api/docs/yorkie/v1/admin.openapi.yaml +++ b/api/docs/yorkie/v1/admin.openapi.yaml @@ -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: "" @@ -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 @@ -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: "" diff --git a/api/docs/yorkie/v1/cluster.openapi.yaml b/api/docs/yorkie/v1/cluster.openapi.yaml index ffc984cac..dde26f1fb 100644 --- a/api/docs/yorkie/v1/cluster.openapi.yaml +++ b/api/docs/yorkie/v1/cluster.openapi.yaml @@ -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: "" diff --git a/api/docs/yorkie/v1/resources.openapi.yaml b/api/docs/yorkie/v1/resources.openapi.yaml index 4459cd269..e3bf21dea 100644 --- a/api/docs/yorkie/v1/resources.openapi.yaml +++ b/api/docs/yorkie/v1/resources.openapi.yaml @@ -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: "" @@ -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 @@ -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: "" diff --git a/api/types/event_webhook.go b/api/types/event_webhook.go new file mode 100644 index 000000000..21d70a54c --- /dev/null +++ b/api/types/event_webhook.go @@ -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"` +} diff --git a/api/types/events/events.go b/api/types/events/events.go index 93add0e4a..1735a6f4b 100644 --- a/api/types/events/events.go +++ b/api/types/events/events.go @@ -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" @@ -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 diff --git a/api/types/project.go b/api/types/project.go index 7095cb830..1244fcc35 100644 --- a/api/types/project.go +++ b/api/types/project.go @@ -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"` @@ -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 +} diff --git a/api/types/project_test.go b/api/types/project_test.go index cca5923b9..d4503b068 100644 --- a/api/types/project_test.go +++ b/api/types/project_test.go @@ -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)) + }) } diff --git a/api/types/updatable_project_fields.go b/api/types/updatable_project_fields.go index ef3897973..73267dfe4 100644 --- a/api/types/updatable_project_fields.go +++ b/api/types/updatable_project_fields.go @@ -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 } @@ -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) + } } diff --git a/api/types/updatable_project_fields_test.go b/api/types/updatable_project_fields_test.go index e8e61263b..c461c249a 100644 --- a/api/types/updatable_project_fields_test.go +++ b/api/types/updatable_project_fields_test.go @@ -34,11 +34,17 @@ func TestUpdatableProjectFields(t *testing.T) { string(types.AttachDocument), string(types.WatchDocuments), } + newEventWebhookURL := "http://localhost:4000" + newEventWebhookEvents := []string{ + string(types.DocRootChanged), + } newClientDeactivateThreshold := "1h" fields := &types.UpdatableProjectFields{ Name: &newName, AuthWebhookURL: &newAuthWebhookURL, AuthWebhookMethods: &newAuthWebhookMethods, + EventWebhookURL: &newEventWebhookURL, + EventWebhookEvents: &newEventWebhookEvents, ClientDeactivateThreshold: &newClientDeactivateThreshold, } assert.NoError(t, fields.Validate()) @@ -54,6 +60,7 @@ func TestUpdatableProjectFields(t *testing.T) { fields = &types.UpdatableProjectFields{ Name: &newName, AuthWebhookURL: &newAuthWebhookURL, + EventWebhookURL: &newEventWebhookURL, ClientDeactivateThreshold: &newClientDeactivateThreshold, } assert.NoError(t, fields.Validate()) @@ -62,10 +69,15 @@ func TestUpdatableProjectFields(t *testing.T) { newAuthWebhookMethods = []string{ "InvalidMethods", } + // invalid EventWebhookEvents + newEventWebhookEvents = []string{ + "DocChanged", + } fields = &types.UpdatableProjectFields{ Name: &newName, AuthWebhookURL: &newAuthWebhookURL, AuthWebhookMethods: &newAuthWebhookMethods, + EventWebhookEvents: &newEventWebhookEvents, ClientDeactivateThreshold: &newClientDeactivateThreshold, } assert.ErrorAs(t, fields.Validate(), &formErr) diff --git a/api/yorkie/v1/resources.pb.go b/api/yorkie/v1/resources.pb.go index 3b49d3086..59cd91091 100644 --- a/api/yorkie/v1/resources.pb.go +++ b/api/yorkie/v1/resources.pb.go @@ -1639,9 +1639,11 @@ type Project struct { SecretKey string `protobuf:"bytes,4,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"` AuthWebhookUrl string `protobuf:"bytes,5,opt,name=auth_webhook_url,json=authWebhookUrl,proto3" json:"auth_webhook_url,omitempty"` AuthWebhookMethods []string `protobuf:"bytes,6,rep,name=auth_webhook_methods,json=authWebhookMethods,proto3" json:"auth_webhook_methods,omitempty"` - ClientDeactivateThreshold string `protobuf:"bytes,7,opt,name=client_deactivate_threshold,json=clientDeactivateThreshold,proto3" json:"client_deactivate_threshold,omitempty"` - CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + EventWebhookUrl string `protobuf:"bytes,7,opt,name=event_webhook_url,json=eventWebhookUrl,proto3" json:"event_webhook_url,omitempty"` + EventWebhookEvents []string `protobuf:"bytes,8,rep,name=event_webhook_events,json=eventWebhookEvents,proto3" json:"event_webhook_events,omitempty"` + ClientDeactivateThreshold string `protobuf:"bytes,9,opt,name=client_deactivate_threshold,json=clientDeactivateThreshold,proto3" json:"client_deactivate_threshold,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` } func (x *Project) Reset() { @@ -1718,6 +1720,20 @@ func (x *Project) GetAuthWebhookMethods() []string { return nil } +func (x *Project) GetEventWebhookUrl() string { + if x != nil { + return x.EventWebhookUrl + } + return "" +} + +func (x *Project) GetEventWebhookEvents() []string { + if x != nil { + return x.EventWebhookEvents + } + return nil +} + func (x *Project) GetClientDeactivateThreshold() string { if x != nil { return x.ClientDeactivateThreshold @@ -1747,7 +1763,9 @@ type UpdatableProjectFields struct { Name *wrapperspb.StringValue `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` AuthWebhookUrl *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=auth_webhook_url,json=authWebhookUrl,proto3" json:"auth_webhook_url,omitempty"` AuthWebhookMethods *UpdatableProjectFields_AuthWebhookMethods `protobuf:"bytes,3,opt,name=auth_webhook_methods,json=authWebhookMethods,proto3" json:"auth_webhook_methods,omitempty"` - ClientDeactivateThreshold *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=client_deactivate_threshold,json=clientDeactivateThreshold,proto3" json:"client_deactivate_threshold,omitempty"` + EventWebhookUrl *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=event_webhook_url,json=eventWebhookUrl,proto3" json:"event_webhook_url,omitempty"` + EventWebhookEvents *UpdatableProjectFields_EventWebhookEvents `protobuf:"bytes,5,opt,name=event_webhook_events,json=eventWebhookEvents,proto3" json:"event_webhook_events,omitempty"` + ClientDeactivateThreshold *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=client_deactivate_threshold,json=clientDeactivateThreshold,proto3" json:"client_deactivate_threshold,omitempty"` } func (x *UpdatableProjectFields) Reset() { @@ -1803,6 +1821,20 @@ func (x *UpdatableProjectFields) GetAuthWebhookMethods() *UpdatableProjectFields return nil } +func (x *UpdatableProjectFields) GetEventWebhookUrl() *wrapperspb.StringValue { + if x != nil { + return x.EventWebhookUrl + } + return nil +} + +func (x *UpdatableProjectFields) GetEventWebhookEvents() *UpdatableProjectFields_EventWebhookEvents { + if x != nil { + return x.EventWebhookEvents + } + return nil +} + func (x *UpdatableProjectFields) GetClientDeactivateThreshold() *wrapperspb.StringValue { if x != nil { return x.ClientDeactivateThreshold @@ -3644,6 +3676,53 @@ func (x *UpdatableProjectFields_AuthWebhookMethods) GetMethods() []string { return nil } +type UpdatableProjectFields_EventWebhookEvents struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Events []string `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` +} + +func (x *UpdatableProjectFields_EventWebhookEvents) Reset() { + *x = UpdatableProjectFields_EventWebhookEvents{} + if protoimpl.UnsafeEnabled { + mi := &file_yorkie_v1_resources_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdatableProjectFields_EventWebhookEvents) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdatableProjectFields_EventWebhookEvents) ProtoMessage() {} + +func (x *UpdatableProjectFields_EventWebhookEvents) ProtoReflect() protoreflect.Message { + mi := &file_yorkie_v1_resources_proto_msgTypes[57] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdatableProjectFields_EventWebhookEvents.ProtoReflect.Descriptor instead. +func (*UpdatableProjectFields_EventWebhookEvents) Descriptor() ([]byte, []int) { + return file_yorkie_v1_resources_proto_rawDescGZIP(), []int{19, 1} +} + +func (x *UpdatableProjectFields_EventWebhookEvents) GetEvents() []string { + if x != nil { + return x.Events + } + return nil +} + var File_yorkie_v1_resources_proto protoreflect.FileDescriptor var file_yorkie_v1_resources_proto_rawDesc = []byte{ @@ -4214,8 +4293,8 @@ var file_yorkie_v1_resources_proto_rawDesc = []byte{ 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xfd, - 0x02, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xdb, + 0x03, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, @@ -4227,150 +4306,170 @@ var file_yorkie_v1_resources_proto_rawDesc = []byte{ 0x6f, 0x6f, 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, - 0x6b, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x3e, 0x0a, 0x1b, 0x63, 0x6c, 0x69, 0x65, - 0x6e, 0x74, 0x5f, 0x64, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x68, - 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x54, - 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, - 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x88, - 0x03, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x30, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x61, - 0x75, 0x74, 0x68, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x6b, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x65, 0x76, 0x65, 0x6e, + 0x74, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x55, 0x72, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x77, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x12, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3e, 0x0a, 0x1b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x64, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x68, 0x72, 0x65, + 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x54, 0x68, 0x72, + 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xe8, 0x04, 0x0a, + 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x30, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, - 0x55, 0x72, 0x6c, 0x12, 0x66, 0x0a, 0x14, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x77, 0x65, 0x62, 0x68, - 0x6f, 0x6f, 0x6b, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x6c, 0x75, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x61, 0x75, 0x74, + 0x68, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x72, + 0x6c, 0x12, 0x66, 0x0a, 0x14, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x34, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x73, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x57, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x48, 0x0a, 0x11, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x0f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, + 0x55, 0x72, 0x6c, 0x12, 0x66, 0x0a, 0x14, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x77, 0x65, 0x62, + 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x46, 0x69, - 0x65, 0x6c, 0x64, 0x73, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x57, 0x65, 0x62, - 0x68, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x5c, 0x0a, 0x1b, 0x63, + 0x65, 0x6c, 0x64, 0x73, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x12, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x57, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x5c, 0x0a, 0x1b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x19, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x1a, 0x2e, 0x0a, 0x12, 0x41, 0x75, 0x74, 0x68, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x07, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x22, 0x82, 0x02, 0x0a, 0x0f, 0x44, 0x6f, - 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x1a, 0x0a, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, - 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, - 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xea, - 0x01, 0x0a, 0x0e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x12, 0x38, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x24, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x73, - 0x65, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x70, - 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x52, 0x07, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x1a, 0x2c, 0x0a, 0x12, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, + 0x16, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x82, 0x02, 0x0a, 0x0f, 0x44, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, + 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0xea, 0x01, 0x0a, + 0x0e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, + 0x38, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, - 0x63, 0x65, 0x52, 0x08, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x6d, 0x0a, 0x0a, - 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x48, - 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x48, 0x41, 0x4e, 0x47, - 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x55, 0x54, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, - 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, - 0x54, 0x45, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x45, 0x41, 0x52, 0x10, 0x03, 0x22, 0x76, 0x0a, 0x08, 0x50, - 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x44, 0x61, - 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0x4a, 0x0a, 0x0a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x71, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, 0x71, - 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x71, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x71, 0x22, - 0x84, 0x01, 0x0a, 0x0b, 0x54, 0x65, 0x78, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x6f, 0x73, 0x12, - 0x34, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x27, 0x0a, - 0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, - 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x22, 0x5f, 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x54, 0x69, - 0x63, 0x6b, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1c, - 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x12, 0x19, 0x0a, 0x08, - 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, - 0x61, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x3e, 0x0a, 0x0c, 0x44, 0x6f, 0x63, 0x45, 0x76, - 0x65, 0x6e, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x18, 0x0a, - 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, - 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x82, 0x01, 0x0a, 0x08, 0x44, 0x6f, 0x63, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, - 0x6f, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x12, - 0x2b, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x63, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x42, 0x6f, 0x64, 0x79, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x2a, 0xd4, 0x02, 0x0a, - 0x09, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x56, 0x41, - 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x55, 0x4c, 0x4c, 0x10, 0x00, 0x12, - 0x16, 0x0a, 0x12, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x4f, - 0x4f, 0x4c, 0x45, 0x41, 0x4e, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x56, 0x41, 0x4c, 0x55, 0x45, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, 0x10, 0x02, 0x12, - 0x13, 0x0a, 0x0f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4c, 0x4f, - 0x4e, 0x47, 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, 0x56, - 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, - 0x10, 0x05, 0x12, 0x14, 0x0a, 0x10, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x42, 0x59, 0x54, 0x45, 0x53, 0x10, 0x06, 0x12, 0x13, 0x0a, 0x0f, 0x56, 0x41, 0x4c, 0x55, - 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x41, 0x54, 0x45, 0x10, 0x07, 0x12, 0x1a, 0x0a, - 0x16, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, - 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x08, 0x12, 0x19, 0x0a, 0x15, 0x56, 0x41, 0x4c, - 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x5f, 0x41, 0x52, 0x52, - 0x41, 0x59, 0x10, 0x09, 0x12, 0x13, 0x0a, 0x0f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x5f, 0x54, 0x45, 0x58, 0x54, 0x10, 0x0a, 0x12, 0x1a, 0x0a, 0x16, 0x56, 0x41, 0x4c, - 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, 0x5f, - 0x43, 0x4e, 0x54, 0x10, 0x0b, 0x12, 0x17, 0x0a, 0x13, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x4c, 0x4f, 0x4e, 0x47, 0x5f, 0x43, 0x4e, 0x54, 0x10, 0x0c, 0x12, 0x13, - 0x0a, 0x0f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x52, 0x45, - 0x45, 0x10, 0x0d, 0x2a, 0xa6, 0x01, 0x0a, 0x0c, 0x44, 0x6f, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x4f, 0x43, 0x5f, 0x45, 0x56, 0x45, 0x4e, - 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x43, 0x55, 0x4d, 0x45, 0x4e, 0x54, 0x5f, - 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x4f, 0x43, - 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x43, 0x55, - 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x57, 0x41, 0x54, 0x43, 0x48, 0x45, 0x44, 0x10, 0x01, 0x12, 0x25, - 0x0a, 0x21, 0x44, 0x4f, 0x43, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x44, 0x4f, 0x43, 0x55, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x57, 0x41, 0x54, 0x43, - 0x48, 0x45, 0x44, 0x10, 0x02, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x4f, 0x43, 0x5f, 0x45, 0x56, 0x45, - 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x43, 0x55, 0x4d, 0x45, 0x4e, 0x54, - 0x5f, 0x42, 0x52, 0x4f, 0x41, 0x44, 0x43, 0x41, 0x53, 0x54, 0x10, 0x03, 0x42, 0x45, 0x0a, 0x11, - 0x64, 0x65, 0x76, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x31, 0x50, 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2d, 0x74, 0x65, 0x61, 0x6d, 0x2f, 0x79, 0x6f, 0x72, 0x6b, - 0x69, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2f, 0x76, 0x31, - 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x70, 0x72, 0x65, + 0x73, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x79, 0x6f, + 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x08, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x6d, 0x0a, 0x0a, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x48, 0x41, 0x4e, + 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x55, 0x54, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x48, + 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, + 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x43, 0x4c, 0x45, 0x41, 0x52, 0x10, 0x03, 0x22, 0x76, 0x0a, 0x08, 0x50, 0x72, 0x65, + 0x73, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x31, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0x4a, 0x0a, 0x0a, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x71, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, 0x71, 0x12, 0x1d, + 0x0a, 0x0a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x71, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x71, 0x22, 0x84, 0x01, + 0x0a, 0x0b, 0x54, 0x65, 0x78, 0x74, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x6f, 0x73, 0x12, 0x34, 0x0a, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x72, + 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x4f, 0x66, + 0x66, 0x73, 0x65, 0x74, 0x22, 0x5f, 0x0a, 0x0a, 0x54, 0x69, 0x6d, 0x65, 0x54, 0x69, 0x63, 0x6b, + 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x07, 0x6c, 0x61, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x64, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x09, 0x64, 0x65, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x65, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x63, + 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x63, + 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x3e, 0x0a, 0x0c, 0x44, 0x6f, 0x63, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x70, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x82, 0x01, 0x0a, 0x08, 0x44, 0x6f, 0x63, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x12, 0x2b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x17, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x63, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x12, 0x2b, 0x0a, + 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x79, 0x6f, + 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x42, 0x6f, 0x64, 0x79, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x2a, 0xd4, 0x02, 0x0a, 0x09, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x56, 0x41, 0x4c, 0x55, + 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x55, 0x4c, 0x4c, 0x10, 0x00, 0x12, 0x16, 0x0a, + 0x12, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x4f, 0x4f, 0x4c, + 0x45, 0x41, 0x4e, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, 0x10, 0x02, 0x12, 0x13, 0x0a, + 0x0f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4c, 0x4f, 0x4e, 0x47, + 0x10, 0x03, 0x12, 0x15, 0x0a, 0x11, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x44, 0x4f, 0x55, 0x42, 0x4c, 0x45, 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, 0x56, 0x41, 0x4c, + 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x05, + 0x12, 0x14, 0x0a, 0x10, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, + 0x59, 0x54, 0x45, 0x53, 0x10, 0x06, 0x12, 0x13, 0x0a, 0x0f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x41, 0x54, 0x45, 0x10, 0x07, 0x12, 0x1a, 0x0a, 0x16, 0x56, + 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x5f, 0x4f, + 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x08, 0x12, 0x19, 0x0a, 0x15, 0x56, 0x41, 0x4c, 0x55, 0x45, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x5f, 0x41, 0x52, 0x52, 0x41, 0x59, + 0x10, 0x09, 0x12, 0x13, 0x0a, 0x0f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x54, 0x45, 0x58, 0x54, 0x10, 0x0a, 0x12, 0x1a, 0x0a, 0x16, 0x56, 0x41, 0x4c, 0x55, 0x45, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, 0x5f, 0x43, 0x4e, + 0x54, 0x10, 0x0b, 0x12, 0x17, 0x0a, 0x13, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x4c, 0x4f, 0x4e, 0x47, 0x5f, 0x43, 0x4e, 0x54, 0x10, 0x0c, 0x12, 0x13, 0x0a, 0x0f, + 0x56, 0x41, 0x4c, 0x55, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x52, 0x45, 0x45, 0x10, + 0x0d, 0x2a, 0xa6, 0x01, 0x0a, 0x0c, 0x44, 0x6f, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x4f, 0x43, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x43, 0x55, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x48, + 0x41, 0x4e, 0x47, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x4f, 0x43, 0x5f, 0x45, + 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x43, 0x55, 0x4d, 0x45, + 0x4e, 0x54, 0x5f, 0x57, 0x41, 0x54, 0x43, 0x48, 0x45, 0x44, 0x10, 0x01, 0x12, 0x25, 0x0a, 0x21, + 0x44, 0x4f, 0x43, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, + 0x4f, 0x43, 0x55, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x57, 0x41, 0x54, 0x43, 0x48, 0x45, + 0x44, 0x10, 0x02, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x4f, 0x43, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x43, 0x55, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x42, + 0x52, 0x4f, 0x41, 0x44, 0x43, 0x41, 0x53, 0x54, 0x10, 0x03, 0x42, 0x45, 0x0a, 0x11, 0x64, 0x65, + 0x76, 0x2e, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x50, + 0x01, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x79, 0x6f, + 0x72, 0x6b, 0x69, 0x65, 0x2d, 0x74, 0x65, 0x61, 0x6d, 0x2f, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x79, 0x6f, 0x72, 0x6b, 0x69, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x76, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -4386,7 +4485,7 @@ func file_yorkie_v1_resources_proto_rawDescGZIP() []byte { } var file_yorkie_v1_resources_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_yorkie_v1_resources_proto_msgTypes = make([]protoimpl.MessageInfo, 58) +var file_yorkie_v1_resources_proto_msgTypes = make([]protoimpl.MessageInfo, 59) var file_yorkie_v1_resources_proto_goTypes = []interface{}{ (ValueType)(0), // 0: yorkie.v1.ValueType (DocEventType)(0), // 1: yorkie.v1.DocEventType @@ -4448,9 +4547,10 @@ var file_yorkie_v1_resources_proto_goTypes = []interface{}{ nil, // 57: yorkie.v1.TextNode.AttributesEntry nil, // 58: yorkie.v1.TreeNode.AttributesEntry (*UpdatableProjectFields_AuthWebhookMethods)(nil), // 59: yorkie.v1.UpdatableProjectFields.AuthWebhookMethods - nil, // 60: yorkie.v1.Presence.DataEntry - (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp - (*wrapperspb.StringValue)(nil), // 62: google.protobuf.StringValue + (*UpdatableProjectFields_EventWebhookEvents)(nil), // 60: yorkie.v1.UpdatableProjectFields.EventWebhookEvents + nil, // 61: yorkie.v1.Presence.DataEntry + (*timestamppb.Timestamp)(nil), // 62: google.protobuf.Timestamp + (*wrapperspb.StringValue)(nil), // 63: google.protobuf.StringValue } var file_yorkie_v1_resources_proto_depIdxs = []int32{ 10, // 0: yorkie.v1.Snapshot.root:type_name -> yorkie.v1.JSONElement @@ -4503,107 +4603,109 @@ var file_yorkie_v1_resources_proto_depIdxs = []int32{ 28, // 47: yorkie.v1.TreeNodeID.created_at:type_name -> yorkie.v1.TimeTicket 18, // 48: yorkie.v1.TreePos.parent_id:type_name -> yorkie.v1.TreeNodeID 18, // 49: yorkie.v1.TreePos.left_sibling_id:type_name -> yorkie.v1.TreeNodeID - 61, // 50: yorkie.v1.User.created_at:type_name -> google.protobuf.Timestamp - 61, // 51: yorkie.v1.Project.created_at:type_name -> google.protobuf.Timestamp - 61, // 52: yorkie.v1.Project.updated_at:type_name -> google.protobuf.Timestamp - 62, // 53: yorkie.v1.UpdatableProjectFields.name:type_name -> google.protobuf.StringValue - 62, // 54: yorkie.v1.UpdatableProjectFields.auth_webhook_url:type_name -> google.protobuf.StringValue + 62, // 50: yorkie.v1.User.created_at:type_name -> google.protobuf.Timestamp + 62, // 51: yorkie.v1.Project.created_at:type_name -> google.protobuf.Timestamp + 62, // 52: yorkie.v1.Project.updated_at:type_name -> google.protobuf.Timestamp + 63, // 53: yorkie.v1.UpdatableProjectFields.name:type_name -> google.protobuf.StringValue + 63, // 54: yorkie.v1.UpdatableProjectFields.auth_webhook_url:type_name -> google.protobuf.StringValue 59, // 55: yorkie.v1.UpdatableProjectFields.auth_webhook_methods:type_name -> yorkie.v1.UpdatableProjectFields.AuthWebhookMethods - 62, // 56: yorkie.v1.UpdatableProjectFields.client_deactivate_threshold:type_name -> google.protobuf.StringValue - 61, // 57: yorkie.v1.DocumentSummary.created_at:type_name -> google.protobuf.Timestamp - 61, // 58: yorkie.v1.DocumentSummary.accessed_at:type_name -> google.protobuf.Timestamp - 61, // 59: yorkie.v1.DocumentSummary.updated_at:type_name -> google.protobuf.Timestamp - 2, // 60: yorkie.v1.PresenceChange.type:type_name -> yorkie.v1.PresenceChange.ChangeType - 25, // 61: yorkie.v1.PresenceChange.presence:type_name -> yorkie.v1.Presence - 60, // 62: yorkie.v1.Presence.data:type_name -> yorkie.v1.Presence.DataEntry - 28, // 63: yorkie.v1.TextNodePos.created_at:type_name -> yorkie.v1.TimeTicket - 1, // 64: yorkie.v1.DocEvent.type:type_name -> yorkie.v1.DocEventType - 29, // 65: yorkie.v1.DocEvent.body:type_name -> yorkie.v1.DocEventBody - 25, // 66: yorkie.v1.Snapshot.PresencesEntry.value:type_name -> yorkie.v1.Presence - 28, // 67: yorkie.v1.Operation.Set.parent_created_at:type_name -> yorkie.v1.TimeTicket - 9, // 68: yorkie.v1.Operation.Set.value:type_name -> yorkie.v1.JSONElementSimple - 28, // 69: yorkie.v1.Operation.Set.executed_at:type_name -> yorkie.v1.TimeTicket - 28, // 70: yorkie.v1.Operation.Add.parent_created_at:type_name -> yorkie.v1.TimeTicket - 28, // 71: yorkie.v1.Operation.Add.prev_created_at:type_name -> yorkie.v1.TimeTicket - 9, // 72: yorkie.v1.Operation.Add.value:type_name -> yorkie.v1.JSONElementSimple - 28, // 73: yorkie.v1.Operation.Add.executed_at:type_name -> yorkie.v1.TimeTicket - 28, // 74: yorkie.v1.Operation.Move.parent_created_at:type_name -> yorkie.v1.TimeTicket - 28, // 75: yorkie.v1.Operation.Move.prev_created_at:type_name -> yorkie.v1.TimeTicket - 28, // 76: yorkie.v1.Operation.Move.created_at:type_name -> yorkie.v1.TimeTicket - 28, // 77: yorkie.v1.Operation.Move.executed_at:type_name -> yorkie.v1.TimeTicket - 28, // 78: yorkie.v1.Operation.Remove.parent_created_at:type_name -> yorkie.v1.TimeTicket - 28, // 79: yorkie.v1.Operation.Remove.created_at:type_name -> yorkie.v1.TimeTicket - 28, // 80: yorkie.v1.Operation.Remove.executed_at:type_name -> yorkie.v1.TimeTicket - 28, // 81: yorkie.v1.Operation.Edit.parent_created_at:type_name -> yorkie.v1.TimeTicket - 27, // 82: yorkie.v1.Operation.Edit.from:type_name -> yorkie.v1.TextNodePos - 27, // 83: yorkie.v1.Operation.Edit.to:type_name -> yorkie.v1.TextNodePos - 44, // 84: yorkie.v1.Operation.Edit.created_at_map_by_actor:type_name -> yorkie.v1.Operation.Edit.CreatedAtMapByActorEntry - 28, // 85: yorkie.v1.Operation.Edit.executed_at:type_name -> yorkie.v1.TimeTicket - 45, // 86: yorkie.v1.Operation.Edit.attributes:type_name -> yorkie.v1.Operation.Edit.AttributesEntry - 28, // 87: yorkie.v1.Operation.Select.parent_created_at:type_name -> yorkie.v1.TimeTicket - 27, // 88: yorkie.v1.Operation.Select.from:type_name -> yorkie.v1.TextNodePos - 27, // 89: yorkie.v1.Operation.Select.to:type_name -> yorkie.v1.TextNodePos - 28, // 90: yorkie.v1.Operation.Select.executed_at:type_name -> yorkie.v1.TimeTicket - 28, // 91: yorkie.v1.Operation.Style.parent_created_at:type_name -> yorkie.v1.TimeTicket - 27, // 92: yorkie.v1.Operation.Style.from:type_name -> yorkie.v1.TextNodePos - 27, // 93: yorkie.v1.Operation.Style.to:type_name -> yorkie.v1.TextNodePos - 46, // 94: yorkie.v1.Operation.Style.attributes:type_name -> yorkie.v1.Operation.Style.AttributesEntry - 28, // 95: yorkie.v1.Operation.Style.executed_at:type_name -> yorkie.v1.TimeTicket - 47, // 96: yorkie.v1.Operation.Style.created_at_map_by_actor:type_name -> yorkie.v1.Operation.Style.CreatedAtMapByActorEntry - 28, // 97: yorkie.v1.Operation.Increase.parent_created_at:type_name -> yorkie.v1.TimeTicket - 9, // 98: yorkie.v1.Operation.Increase.value:type_name -> yorkie.v1.JSONElementSimple - 28, // 99: yorkie.v1.Operation.Increase.executed_at:type_name -> yorkie.v1.TimeTicket - 28, // 100: yorkie.v1.Operation.TreeEdit.parent_created_at:type_name -> yorkie.v1.TimeTicket - 19, // 101: yorkie.v1.Operation.TreeEdit.from:type_name -> yorkie.v1.TreePos - 19, // 102: yorkie.v1.Operation.TreeEdit.to:type_name -> yorkie.v1.TreePos - 48, // 103: yorkie.v1.Operation.TreeEdit.created_at_map_by_actor:type_name -> yorkie.v1.Operation.TreeEdit.CreatedAtMapByActorEntry - 17, // 104: yorkie.v1.Operation.TreeEdit.contents:type_name -> yorkie.v1.TreeNodes - 28, // 105: yorkie.v1.Operation.TreeEdit.executed_at:type_name -> yorkie.v1.TimeTicket - 28, // 106: yorkie.v1.Operation.TreeStyle.parent_created_at:type_name -> yorkie.v1.TimeTicket - 19, // 107: yorkie.v1.Operation.TreeStyle.from:type_name -> yorkie.v1.TreePos - 19, // 108: yorkie.v1.Operation.TreeStyle.to:type_name -> yorkie.v1.TreePos - 49, // 109: yorkie.v1.Operation.TreeStyle.attributes:type_name -> yorkie.v1.Operation.TreeStyle.AttributesEntry - 28, // 110: yorkie.v1.Operation.TreeStyle.executed_at:type_name -> yorkie.v1.TimeTicket - 50, // 111: yorkie.v1.Operation.TreeStyle.created_at_map_by_actor:type_name -> yorkie.v1.Operation.TreeStyle.CreatedAtMapByActorEntry - 28, // 112: yorkie.v1.Operation.ArraySet.parent_created_at:type_name -> yorkie.v1.TimeTicket - 28, // 113: yorkie.v1.Operation.ArraySet.created_at:type_name -> yorkie.v1.TimeTicket - 9, // 114: yorkie.v1.Operation.ArraySet.value:type_name -> yorkie.v1.JSONElementSimple - 28, // 115: yorkie.v1.Operation.ArraySet.executed_at:type_name -> yorkie.v1.TimeTicket - 28, // 116: yorkie.v1.Operation.Edit.CreatedAtMapByActorEntry.value:type_name -> yorkie.v1.TimeTicket - 28, // 117: yorkie.v1.Operation.Style.CreatedAtMapByActorEntry.value:type_name -> yorkie.v1.TimeTicket - 28, // 118: yorkie.v1.Operation.TreeEdit.CreatedAtMapByActorEntry.value:type_name -> yorkie.v1.TimeTicket - 28, // 119: yorkie.v1.Operation.TreeStyle.CreatedAtMapByActorEntry.value:type_name -> yorkie.v1.TimeTicket - 11, // 120: yorkie.v1.JSONElement.JSONObject.nodes:type_name -> yorkie.v1.RHTNode - 28, // 121: yorkie.v1.JSONElement.JSONObject.created_at:type_name -> yorkie.v1.TimeTicket - 28, // 122: yorkie.v1.JSONElement.JSONObject.moved_at:type_name -> yorkie.v1.TimeTicket - 28, // 123: yorkie.v1.JSONElement.JSONObject.removed_at:type_name -> yorkie.v1.TimeTicket - 12, // 124: yorkie.v1.JSONElement.JSONArray.nodes:type_name -> yorkie.v1.RGANode - 28, // 125: yorkie.v1.JSONElement.JSONArray.created_at:type_name -> yorkie.v1.TimeTicket - 28, // 126: yorkie.v1.JSONElement.JSONArray.moved_at:type_name -> yorkie.v1.TimeTicket - 28, // 127: yorkie.v1.JSONElement.JSONArray.removed_at:type_name -> yorkie.v1.TimeTicket - 0, // 128: yorkie.v1.JSONElement.Primitive.type:type_name -> yorkie.v1.ValueType - 28, // 129: yorkie.v1.JSONElement.Primitive.created_at:type_name -> yorkie.v1.TimeTicket - 28, // 130: yorkie.v1.JSONElement.Primitive.moved_at:type_name -> yorkie.v1.TimeTicket - 28, // 131: yorkie.v1.JSONElement.Primitive.removed_at:type_name -> yorkie.v1.TimeTicket - 14, // 132: yorkie.v1.JSONElement.Text.nodes:type_name -> yorkie.v1.TextNode - 28, // 133: yorkie.v1.JSONElement.Text.created_at:type_name -> yorkie.v1.TimeTicket - 28, // 134: yorkie.v1.JSONElement.Text.moved_at:type_name -> yorkie.v1.TimeTicket - 28, // 135: yorkie.v1.JSONElement.Text.removed_at:type_name -> yorkie.v1.TimeTicket - 0, // 136: yorkie.v1.JSONElement.Counter.type:type_name -> yorkie.v1.ValueType - 28, // 137: yorkie.v1.JSONElement.Counter.created_at:type_name -> yorkie.v1.TimeTicket - 28, // 138: yorkie.v1.JSONElement.Counter.moved_at:type_name -> yorkie.v1.TimeTicket - 28, // 139: yorkie.v1.JSONElement.Counter.removed_at:type_name -> yorkie.v1.TimeTicket - 16, // 140: yorkie.v1.JSONElement.Tree.nodes:type_name -> yorkie.v1.TreeNode - 28, // 141: yorkie.v1.JSONElement.Tree.created_at:type_name -> yorkie.v1.TimeTicket - 28, // 142: yorkie.v1.JSONElement.Tree.moved_at:type_name -> yorkie.v1.TimeTicket - 28, // 143: yorkie.v1.JSONElement.Tree.removed_at:type_name -> yorkie.v1.TimeTicket - 13, // 144: yorkie.v1.TextNode.AttributesEntry.value:type_name -> yorkie.v1.NodeAttr - 13, // 145: yorkie.v1.TreeNode.AttributesEntry.value:type_name -> yorkie.v1.NodeAttr - 146, // [146:146] is the sub-list for method output_type - 146, // [146:146] is the sub-list for method input_type - 146, // [146:146] is the sub-list for extension type_name - 146, // [146:146] is the sub-list for extension extendee - 0, // [0:146] is the sub-list for field type_name + 63, // 56: yorkie.v1.UpdatableProjectFields.event_webhook_url:type_name -> google.protobuf.StringValue + 60, // 57: yorkie.v1.UpdatableProjectFields.event_webhook_events:type_name -> yorkie.v1.UpdatableProjectFields.EventWebhookEvents + 63, // 58: yorkie.v1.UpdatableProjectFields.client_deactivate_threshold:type_name -> google.protobuf.StringValue + 62, // 59: yorkie.v1.DocumentSummary.created_at:type_name -> google.protobuf.Timestamp + 62, // 60: yorkie.v1.DocumentSummary.accessed_at:type_name -> google.protobuf.Timestamp + 62, // 61: yorkie.v1.DocumentSummary.updated_at:type_name -> google.protobuf.Timestamp + 2, // 62: yorkie.v1.PresenceChange.type:type_name -> yorkie.v1.PresenceChange.ChangeType + 25, // 63: yorkie.v1.PresenceChange.presence:type_name -> yorkie.v1.Presence + 61, // 64: yorkie.v1.Presence.data:type_name -> yorkie.v1.Presence.DataEntry + 28, // 65: yorkie.v1.TextNodePos.created_at:type_name -> yorkie.v1.TimeTicket + 1, // 66: yorkie.v1.DocEvent.type:type_name -> yorkie.v1.DocEventType + 29, // 67: yorkie.v1.DocEvent.body:type_name -> yorkie.v1.DocEventBody + 25, // 68: yorkie.v1.Snapshot.PresencesEntry.value:type_name -> yorkie.v1.Presence + 28, // 69: yorkie.v1.Operation.Set.parent_created_at:type_name -> yorkie.v1.TimeTicket + 9, // 70: yorkie.v1.Operation.Set.value:type_name -> yorkie.v1.JSONElementSimple + 28, // 71: yorkie.v1.Operation.Set.executed_at:type_name -> yorkie.v1.TimeTicket + 28, // 72: yorkie.v1.Operation.Add.parent_created_at:type_name -> yorkie.v1.TimeTicket + 28, // 73: yorkie.v1.Operation.Add.prev_created_at:type_name -> yorkie.v1.TimeTicket + 9, // 74: yorkie.v1.Operation.Add.value:type_name -> yorkie.v1.JSONElementSimple + 28, // 75: yorkie.v1.Operation.Add.executed_at:type_name -> yorkie.v1.TimeTicket + 28, // 76: yorkie.v1.Operation.Move.parent_created_at:type_name -> yorkie.v1.TimeTicket + 28, // 77: yorkie.v1.Operation.Move.prev_created_at:type_name -> yorkie.v1.TimeTicket + 28, // 78: yorkie.v1.Operation.Move.created_at:type_name -> yorkie.v1.TimeTicket + 28, // 79: yorkie.v1.Operation.Move.executed_at:type_name -> yorkie.v1.TimeTicket + 28, // 80: yorkie.v1.Operation.Remove.parent_created_at:type_name -> yorkie.v1.TimeTicket + 28, // 81: yorkie.v1.Operation.Remove.created_at:type_name -> yorkie.v1.TimeTicket + 28, // 82: yorkie.v1.Operation.Remove.executed_at:type_name -> yorkie.v1.TimeTicket + 28, // 83: yorkie.v1.Operation.Edit.parent_created_at:type_name -> yorkie.v1.TimeTicket + 27, // 84: yorkie.v1.Operation.Edit.from:type_name -> yorkie.v1.TextNodePos + 27, // 85: yorkie.v1.Operation.Edit.to:type_name -> yorkie.v1.TextNodePos + 44, // 86: yorkie.v1.Operation.Edit.created_at_map_by_actor:type_name -> yorkie.v1.Operation.Edit.CreatedAtMapByActorEntry + 28, // 87: yorkie.v1.Operation.Edit.executed_at:type_name -> yorkie.v1.TimeTicket + 45, // 88: yorkie.v1.Operation.Edit.attributes:type_name -> yorkie.v1.Operation.Edit.AttributesEntry + 28, // 89: yorkie.v1.Operation.Select.parent_created_at:type_name -> yorkie.v1.TimeTicket + 27, // 90: yorkie.v1.Operation.Select.from:type_name -> yorkie.v1.TextNodePos + 27, // 91: yorkie.v1.Operation.Select.to:type_name -> yorkie.v1.TextNodePos + 28, // 92: yorkie.v1.Operation.Select.executed_at:type_name -> yorkie.v1.TimeTicket + 28, // 93: yorkie.v1.Operation.Style.parent_created_at:type_name -> yorkie.v1.TimeTicket + 27, // 94: yorkie.v1.Operation.Style.from:type_name -> yorkie.v1.TextNodePos + 27, // 95: yorkie.v1.Operation.Style.to:type_name -> yorkie.v1.TextNodePos + 46, // 96: yorkie.v1.Operation.Style.attributes:type_name -> yorkie.v1.Operation.Style.AttributesEntry + 28, // 97: yorkie.v1.Operation.Style.executed_at:type_name -> yorkie.v1.TimeTicket + 47, // 98: yorkie.v1.Operation.Style.created_at_map_by_actor:type_name -> yorkie.v1.Operation.Style.CreatedAtMapByActorEntry + 28, // 99: yorkie.v1.Operation.Increase.parent_created_at:type_name -> yorkie.v1.TimeTicket + 9, // 100: yorkie.v1.Operation.Increase.value:type_name -> yorkie.v1.JSONElementSimple + 28, // 101: yorkie.v1.Operation.Increase.executed_at:type_name -> yorkie.v1.TimeTicket + 28, // 102: yorkie.v1.Operation.TreeEdit.parent_created_at:type_name -> yorkie.v1.TimeTicket + 19, // 103: yorkie.v1.Operation.TreeEdit.from:type_name -> yorkie.v1.TreePos + 19, // 104: yorkie.v1.Operation.TreeEdit.to:type_name -> yorkie.v1.TreePos + 48, // 105: yorkie.v1.Operation.TreeEdit.created_at_map_by_actor:type_name -> yorkie.v1.Operation.TreeEdit.CreatedAtMapByActorEntry + 17, // 106: yorkie.v1.Operation.TreeEdit.contents:type_name -> yorkie.v1.TreeNodes + 28, // 107: yorkie.v1.Operation.TreeEdit.executed_at:type_name -> yorkie.v1.TimeTicket + 28, // 108: yorkie.v1.Operation.TreeStyle.parent_created_at:type_name -> yorkie.v1.TimeTicket + 19, // 109: yorkie.v1.Operation.TreeStyle.from:type_name -> yorkie.v1.TreePos + 19, // 110: yorkie.v1.Operation.TreeStyle.to:type_name -> yorkie.v1.TreePos + 49, // 111: yorkie.v1.Operation.TreeStyle.attributes:type_name -> yorkie.v1.Operation.TreeStyle.AttributesEntry + 28, // 112: yorkie.v1.Operation.TreeStyle.executed_at:type_name -> yorkie.v1.TimeTicket + 50, // 113: yorkie.v1.Operation.TreeStyle.created_at_map_by_actor:type_name -> yorkie.v1.Operation.TreeStyle.CreatedAtMapByActorEntry + 28, // 114: yorkie.v1.Operation.ArraySet.parent_created_at:type_name -> yorkie.v1.TimeTicket + 28, // 115: yorkie.v1.Operation.ArraySet.created_at:type_name -> yorkie.v1.TimeTicket + 9, // 116: yorkie.v1.Operation.ArraySet.value:type_name -> yorkie.v1.JSONElementSimple + 28, // 117: yorkie.v1.Operation.ArraySet.executed_at:type_name -> yorkie.v1.TimeTicket + 28, // 118: yorkie.v1.Operation.Edit.CreatedAtMapByActorEntry.value:type_name -> yorkie.v1.TimeTicket + 28, // 119: yorkie.v1.Operation.Style.CreatedAtMapByActorEntry.value:type_name -> yorkie.v1.TimeTicket + 28, // 120: yorkie.v1.Operation.TreeEdit.CreatedAtMapByActorEntry.value:type_name -> yorkie.v1.TimeTicket + 28, // 121: yorkie.v1.Operation.TreeStyle.CreatedAtMapByActorEntry.value:type_name -> yorkie.v1.TimeTicket + 11, // 122: yorkie.v1.JSONElement.JSONObject.nodes:type_name -> yorkie.v1.RHTNode + 28, // 123: yorkie.v1.JSONElement.JSONObject.created_at:type_name -> yorkie.v1.TimeTicket + 28, // 124: yorkie.v1.JSONElement.JSONObject.moved_at:type_name -> yorkie.v1.TimeTicket + 28, // 125: yorkie.v1.JSONElement.JSONObject.removed_at:type_name -> yorkie.v1.TimeTicket + 12, // 126: yorkie.v1.JSONElement.JSONArray.nodes:type_name -> yorkie.v1.RGANode + 28, // 127: yorkie.v1.JSONElement.JSONArray.created_at:type_name -> yorkie.v1.TimeTicket + 28, // 128: yorkie.v1.JSONElement.JSONArray.moved_at:type_name -> yorkie.v1.TimeTicket + 28, // 129: yorkie.v1.JSONElement.JSONArray.removed_at:type_name -> yorkie.v1.TimeTicket + 0, // 130: yorkie.v1.JSONElement.Primitive.type:type_name -> yorkie.v1.ValueType + 28, // 131: yorkie.v1.JSONElement.Primitive.created_at:type_name -> yorkie.v1.TimeTicket + 28, // 132: yorkie.v1.JSONElement.Primitive.moved_at:type_name -> yorkie.v1.TimeTicket + 28, // 133: yorkie.v1.JSONElement.Primitive.removed_at:type_name -> yorkie.v1.TimeTicket + 14, // 134: yorkie.v1.JSONElement.Text.nodes:type_name -> yorkie.v1.TextNode + 28, // 135: yorkie.v1.JSONElement.Text.created_at:type_name -> yorkie.v1.TimeTicket + 28, // 136: yorkie.v1.JSONElement.Text.moved_at:type_name -> yorkie.v1.TimeTicket + 28, // 137: yorkie.v1.JSONElement.Text.removed_at:type_name -> yorkie.v1.TimeTicket + 0, // 138: yorkie.v1.JSONElement.Counter.type:type_name -> yorkie.v1.ValueType + 28, // 139: yorkie.v1.JSONElement.Counter.created_at:type_name -> yorkie.v1.TimeTicket + 28, // 140: yorkie.v1.JSONElement.Counter.moved_at:type_name -> yorkie.v1.TimeTicket + 28, // 141: yorkie.v1.JSONElement.Counter.removed_at:type_name -> yorkie.v1.TimeTicket + 16, // 142: yorkie.v1.JSONElement.Tree.nodes:type_name -> yorkie.v1.TreeNode + 28, // 143: yorkie.v1.JSONElement.Tree.created_at:type_name -> yorkie.v1.TimeTicket + 28, // 144: yorkie.v1.JSONElement.Tree.moved_at:type_name -> yorkie.v1.TimeTicket + 28, // 145: yorkie.v1.JSONElement.Tree.removed_at:type_name -> yorkie.v1.TimeTicket + 13, // 146: yorkie.v1.TextNode.AttributesEntry.value:type_name -> yorkie.v1.NodeAttr + 13, // 147: yorkie.v1.TreeNode.AttributesEntry.value:type_name -> yorkie.v1.NodeAttr + 148, // [148:148] is the sub-list for method output_type + 148, // [148:148] is the sub-list for method input_type + 148, // [148:148] is the sub-list for extension type_name + 148, // [148:148] is the sub-list for extension extendee + 0, // [0:148] is the sub-list for field type_name } func init() { file_yorkie_v1_resources_proto_init() } @@ -5164,6 +5266,18 @@ func file_yorkie_v1_resources_proto_init() { return nil } } + file_yorkie_v1_resources_proto_msgTypes[57].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdatableProjectFields_EventWebhookEvents); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_yorkie_v1_resources_proto_msgTypes[5].OneofWrappers = []interface{}{ (*Operation_Set_)(nil), @@ -5192,7 +5306,7 @@ func file_yorkie_v1_resources_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_yorkie_v1_resources_proto_rawDesc, NumEnums: 3, - NumMessages: 58, + NumMessages: 59, NumExtensions: 0, NumServices: 0, }, diff --git a/api/yorkie/v1/resources.proto b/api/yorkie/v1/resources.proto index 547ec6c55..84757e099 100644 --- a/api/yorkie/v1/resources.proto +++ b/api/yorkie/v1/resources.proto @@ -297,9 +297,11 @@ message Project { string secret_key = 4; string auth_webhook_url = 5; repeated string auth_webhook_methods = 6; - string client_deactivate_threshold = 7; - google.protobuf.Timestamp created_at = 8; - google.protobuf.Timestamp updated_at = 9; + string event_webhook_url = 7; + repeated string event_webhook_events = 8; + string client_deactivate_threshold = 9; + google.protobuf.Timestamp created_at = 10; + google.protobuf.Timestamp updated_at = 11; } message UpdatableProjectFields { @@ -307,10 +309,16 @@ message UpdatableProjectFields { repeated string methods = 1; } + message EventWebhookEvents { + repeated string events = 1; + } + google.protobuf.StringValue name = 1; google.protobuf.StringValue auth_webhook_url = 2; AuthWebhookMethods auth_webhook_methods = 3; - google.protobuf.StringValue client_deactivate_threshold = 4; + google.protobuf.StringValue event_webhook_url = 4; + EventWebhookEvents event_webhook_events = 5; + google.protobuf.StringValue client_deactivate_threshold = 6; } message DocumentSummary { diff --git a/cmd/yorkie/project/list.go b/cmd/yorkie/project/list.go index a05f24d61..14c1bcb85 100644 --- a/cmd/yorkie/project/list.go +++ b/cmd/yorkie/project/list.go @@ -86,6 +86,8 @@ func printProjects(cmd *cobra.Command, output string, projects []*types.Project) "SECRET KEY", "AUTH WEBHOOK URL", "AUTH WEBHOOK METHODS", + "EVENT WEBHOOK URL", + "EVENT WEBHOOK EVENTS", "CLIENT DEACTIVATE THRESHOLD", "CREATED AT", }) @@ -96,6 +98,8 @@ func printProjects(cmd *cobra.Command, output string, projects []*types.Project) project.SecretKey, project.AuthWebhookURL, project.AuthWebhookMethods, + project.EventWebhookURL, + project.EventWebhookEvents, project.ClientDeactivateThreshold, units.HumanDuration(time.Now().UTC().Sub(project.CreatedAt)), }) diff --git a/cmd/yorkie/project/update.go b/cmd/yorkie/project/update.go index 6f0aee096..079c1638a 100644 --- a/cmd/yorkie/project/update.go +++ b/cmd/yorkie/project/update.go @@ -37,6 +37,9 @@ var ( flagAuthWebhookURL string flagAuthWebhookMethodsAdd []string flagAuthWebhookMethodsRm []string + flagEventWebhookURL string + flagEventWebhookEventsAdd []string + flagEventWebhookEventsRm []string flagName string flagClientDeactivateThreshold string ) @@ -52,6 +55,10 @@ var allAuthWebhookMethods = []string{ string(types.Broadcast), } +var allEventWebhookEvents = []string{ + string(types.DocRootChanged), +} + func newUpdateCommand() *cobra.Command { return &cobra.Command{ Use: "update [name]", @@ -95,31 +102,25 @@ func newUpdateCommand() *cobra.Command { newAuthWebhookURL = flagAuthWebhookURL } - methods := make(map[string]struct{}) - for _, m := range project.AuthWebhookMethods { - methods[m] = struct{}{} - } - for _, m := range flagAuthWebhookMethodsRm { - if m == "ALL" { - methods = make(map[string]struct{}) - } else { - delete(methods, m) - } - } - for _, m := range flagAuthWebhookMethodsAdd { - if m == "ALL" { - for _, m := range allAuthWebhookMethods { - methods[m] = struct{}{} - } - } else { - methods[m] = struct{}{} - } - } - newAuthWebhookMethods := make([]string, 0, len(methods)) - for m := range methods { - newAuthWebhookMethods = append(newAuthWebhookMethods, m) + newAuthWebhookMethods := updateStringSlice( + project.AuthWebhookMethods, // prev + flagAuthWebhookMethodsRm, // removes + flagAuthWebhookMethodsAdd, // adds + allAuthWebhookMethods, // all + ) + + newEventWebhookURL := project.EventWebhookURL + if cmd.Flags().Lookup("event-webhook-url").Changed { // allow empty string + newEventWebhookURL = flagEventWebhookURL } + newEventWebhookEvents := updateStringSlice( + project.EventWebhookEvents, // prev + flagEventWebhookEventsRm, // removes + flagEventWebhookEventsAdd, // adds + allEventWebhookEvents, // all + ) + newClientDeactivateThreshold := project.ClientDeactivateThreshold if flagClientDeactivateThreshold != "" { newClientDeactivateThreshold = flagClientDeactivateThreshold @@ -129,6 +130,8 @@ func newUpdateCommand() *cobra.Command { Name: &newName, AuthWebhookURL: &newAuthWebhookURL, AuthWebhookMethods: &newAuthWebhookMethods, + EventWebhookURL: &newEventWebhookURL, + EventWebhookEvents: &newEventWebhookEvents, ClientDeactivateThreshold: &newClientDeactivateThreshold, } @@ -179,6 +182,45 @@ func printUpdateProjectInfo(cmd *cobra.Command, output string, project *types.Pr return nil } +// updateStringSlice updates the string slice with the given items to remove and add. +// If the item is "ALL", it will be replaced with all items. +func updateStringSlice( + prevItems, + itemsToRemove, + itemsToAdd, + allItems []string, +) []string { + items := make(map[string]struct{}) + + for _, p := range prevItems { + items[p] = struct{}{} + } + + for _, r := range itemsToRemove { + if r == "ALL" { + items = make(map[string]struct{}) + } else { + delete(items, r) + } + } + + for _, a := range itemsToAdd { + if a == "ALL" { + for _, m := range allItems { + items[m] = struct{}{} + } + } else { + items[a] = struct{}{} + } + } + + updated := make([]string, 0, len(items)) + for s := range items { + updated = append(updated, s) + } + return updated +} + func init() { cmd := newUpdateCommand() cmd.Flags().StringVar( @@ -205,6 +247,24 @@ func init() { []string{}, "authorization-webhook methods to remove ('ALL' for all methods)", ) + cmd.Flags().StringVar( + &flagEventWebhookURL, + "event-webhook-url", + "", + "event-webhook update url", + ) + cmd.Flags().StringArrayVar( + &flagEventWebhookEventsAdd, + "event-webhook-events-add", + []string{}, + "event-webhook events to add ('ALL' for all events)", + ) + cmd.Flags().StringArrayVar( + &flagEventWebhookEventsRm, + "event-webhook-events-rm", + []string{}, + "event-webhook events to remove ('ALL' for all events)", + ) cmd.Flags().StringVar( &flagClientDeactivateThreshold, "client-deactivate-threshold", diff --git a/cmd/yorkie/server.go b/cmd/yorkie/server.go index 04362600b..357d664cc 100644 --- a/cmd/yorkie/server.go +++ b/cmd/yorkie/server.go @@ -52,7 +52,13 @@ var ( authWebhookMinWaitInterval time.Duration authWebhookRequestTimeout time.Duration authWebhookCacheTTL time.Duration - projectCacheTTL time.Duration + + eventWebhookMaxWaitInterval time.Duration + eventWebhookMinWaitInterval time.Duration + eventWebhookRequestTimeout time.Duration + eventWebhookCacheTTL time.Duration + + projectCacheTTL time.Duration kafkaAddresses string kafkaTopic string @@ -74,6 +80,11 @@ func newServerCmd() *cobra.Command { conf.Backend.AuthWebhookMinWaitInterval = authWebhookMinWaitInterval.String() conf.Backend.AuthWebhookRequestTimeout = authWebhookRequestTimeout.String() conf.Backend.AuthWebhookCacheTTL = authWebhookCacheTTL.String() + + conf.Backend.EventWebhookMaxWaitInterval = eventWebhookMaxWaitInterval.String() + conf.Backend.EventWebhookMinWaitInterval = eventWebhookMinWaitInterval.String() + conf.Backend.EventWebhookRequestTimeout = eventWebhookRequestTimeout.String() + conf.Backend.ProjectCacheTTL = projectCacheTTL.String() conf.Housekeeping.Interval = housekeepingInterval.String() @@ -348,6 +359,30 @@ func init() { server.DefaultAuthWebhookCacheTTL, "TTL value to set when caching authorization webhook response.", ) + cmd.Flags().DurationVar( + &eventWebhookRequestTimeout, + "event-webhook-request-timeout", + server.DefaultEventWebhookRequestTimeout, + "Timeout for each event webhook request.", + ) + cmd.Flags().Uint64Var( + &conf.Backend.EventWebhookMaxRetries, + "event-webhook-max-retries", + server.DefaultEventWebhookMaxRetries, + "Maximum number of retries for event webhook.", + ) + cmd.Flags().DurationVar( + &eventWebhookMinWaitInterval, + "event-webhook-min-wait-interval", + server.DefaultEventWebhookMinWaitInterval, + "Minimum wait interval between retries(exponential backoff).", + ) + cmd.Flags().DurationVar( + &eventWebhookMaxWaitInterval, + "event-webhook-max-wait-interval", + server.DefaultEventWebhookMaxWaitInterval, + "Maximum wait interval between retries(exponential backoff).", + ) cmd.Flags().IntVar( &conf.Backend.ProjectCacheSize, "project-info-cache-size", diff --git a/pkg/webhook/client.go b/pkg/webhook/client.go index f91afa34b..24c28a698 100644 --- a/pkg/webhook/client.go +++ b/pkg/webhook/client.go @@ -59,7 +59,8 @@ type Client[Req any, Res any] struct { options Options } -// NewClient creates a new instance of Client. +// NewClient creates a new instance of Client. If you only want to get the status code, +// then set Res to int. func NewClient[Req any, Res any]( options Options, ) *Client[Req, Res] { @@ -104,6 +105,10 @@ func (c *Client[Req, Res]) Send( return resp.StatusCode, ErrUnexpectedStatusCode } + if _, ok := any(res).(int); ok { + return resp.StatusCode, nil + } + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return resp.StatusCode, ErrUnexpectedResponse } diff --git a/server/backend/backend.go b/server/backend/backend.go index 79fc27991..bc707d9c9 100644 --- a/server/backend/backend.go +++ b/server/backend/backend.go @@ -46,12 +46,15 @@ type Backend struct { Config *Config // AuthWebhookCache is used to cache the response of the auth webhook. - WebhookCache *cache.LRUExpireCache[string, pkgtypes.Pair[ + AuthWebhookCache *cache.LRUExpireCache[string, pkgtypes.Pair[ int, *types.AuthWebhookResponse, ]] - // WebhookClient is used to send auth webhook. - WebhookClient *webhook.Client[types.AuthWebhookRequest, types.AuthWebhookResponse] + // AuthWebhookClient is used to send auth webhook. + AuthWebhookClient *webhook.Client[types.AuthWebhookRequest, types.AuthWebhookResponse] + + // EventWebhookClient is used to send event webhook + EventWebhookClient *webhook.Client[types.EventWebhookRequest, int] // PubSub is used to publish/subscribe events to/from clients. PubSub *pubsub.PubSub @@ -90,11 +93,11 @@ func New( conf.Hostname = hostname } - // 02. Create the webhook webhookCache and client. - webhookCache := cache.NewLRUExpireCache[string, pkgtypes.Pair[int, *types.AuthWebhookResponse]]( + // 02. Create the webhook authWebhookCache and client. + authWebhookCache := cache.NewLRUExpireCache[string, pkgtypes.Pair[int, *types.AuthWebhookResponse]]( conf.AuthWebhookCacheSize, ) - webhookClient := webhook.NewClient[types.AuthWebhookRequest, types.AuthWebhookResponse]( + authWebhookClient := webhook.NewClient[types.AuthWebhookRequest, types.AuthWebhookResponse]( webhook.Options{ MaxRetries: conf.AuthWebhookMaxRetries, MinWaitInterval: conf.ParseAuthWebhookMinWaitInterval(), @@ -103,6 +106,15 @@ func New( }, ) + eventWebhookClient := webhook.NewClient[types.EventWebhookRequest, int]( + webhook.Options{ + MaxRetries: conf.EventWebhookMaxRetries, + MinWaitInterval: conf.ParseEventWebhookMinWaitInterval(), + MaxWaitInterval: conf.ParseEventWebhookMaxWaitInterval(), + RequestTimeout: conf.ParseEventWebhookRequestTimeout(), + }, + ) + // 03. Create pubsub, and locker. locker := sync.New() pubsub := pubsub.New() @@ -165,8 +177,9 @@ func New( return &Backend{ Config: conf, - WebhookCache: webhookCache, - WebhookClient: webhookClient, + AuthWebhookCache: authWebhookCache, + AuthWebhookClient: authWebhookClient, + EventWebhookClient: eventWebhookClient, Locker: locker, PubSub: pubsub, diff --git a/server/backend/config.go b/server/backend/config.go index 9f1767c6c..0133e2043 100644 --- a/server/backend/config.go +++ b/server/backend/config.go @@ -67,7 +67,7 @@ type Config struct { // AuthWebhookMinWaitInterval is the min interval that waits before retrying the authorization webhook. AuthWebhookMinWaitInterval string `yaml:"AuthWebhookMinWaitInterval"` - // AuthWebhookRequestTimeout is the max waiting time per auth webhook request + // AuthWebhookRequestTimeout is the max waiting time per auth webhook request. AuthWebhookRequestTimeout string `yaml:"AuthWebhookRequestTimeout"` // AuthWebhookCacheSize is the cache size of the authorization webhook. @@ -76,6 +76,18 @@ type Config struct { // AuthWebhookCacheTTL is the TTL value to set when caching the authorized result. AuthWebhookCacheTTL string `yaml:"AuthWebhookCacheTTL"` + // EventWebhookMaxRetries is the max count that retries the event webhook. + EventWebhookMaxRetries uint64 `yaml:"EventWebhookMaxRetries"` + + // EventWebhookMaxWaitInterval is the max interval that waits before retrying the event webhook. + EventWebhookMaxWaitInterval string `yaml:"EventWebhookMaxWaitInterval"` + + // EventWebhookMinWaitInterval is the min interval that waits before retrying the event webhook. + EventWebhookMinWaitInterval string `yaml:"EventWebhookMinWaitInterval"` + + // EventWebhookRequestTimeout is the max waiting time per event webhook request. + EventWebhookRequestTimeout string `yaml:"EventWebhookRequestTimeout"` + // ProjectCacheSize is the cache size of the project metadata. ProjectCacheSize int `yaml:"ProjectCacheSize"` @@ -131,6 +143,29 @@ func (c *Config) Validate() error { ) } + if _, err := time.ParseDuration(c.EventWebhookMaxWaitInterval); err != nil { + return fmt.Errorf( + `invalid argument "%s" for "--event-webhook-max-wait-interval" flag: %w`, + c.EventWebhookMaxWaitInterval, + err, + ) + } + + if _, err := time.ParseDuration(c.EventWebhookMinWaitInterval); err != nil { + return fmt.Errorf( + `invalid argument "%s" for "--event-webhook-min-wait-interval" flag: %w`, + c.EventWebhookMinWaitInterval, + err, + ) + } + + if _, err := time.ParseDuration(c.EventWebhookRequestTimeout); err != nil { + return fmt.Errorf( + `invalid argument "%s" for "--event-webhook-request-timeout" flag: %w`, + c.EventWebhookRequestTimeout, + err, + ) + } if _, err := time.ParseDuration(c.ProjectCacheTTL); err != nil { return fmt.Errorf( `invalid argument "%s" for "--project-info-cache-ttl" flag: %w`, @@ -197,6 +232,39 @@ func (c *Config) ParseAuthWebhookCacheTTL() time.Duration { return result } +// ParseEventWebhookMaxWaitInterval returns max wait interval. +func (c *Config) ParseEventWebhookMaxWaitInterval() time.Duration { + result, err := time.ParseDuration(c.EventWebhookMaxWaitInterval) + if err != nil { + fmt.Fprintln(os.Stderr, "parse event webhook max wait interval: %w", err) + os.Exit(1) + } + + return result +} + +// ParseEventWebhookMinWaitInterval returns min wait interval. +func (c *Config) ParseEventWebhookMinWaitInterval() time.Duration { + result, err := time.ParseDuration(c.EventWebhookMinWaitInterval) + if err != nil { + fmt.Fprintln(os.Stderr, "parse event webhook min wait interval: %w", err) + os.Exit(1) + } + + return result +} + +// ParseEventWebhookRequestTimeout returns request timeout. +func (c *Config) ParseEventWebhookRequestTimeout() time.Duration { + result, err := time.ParseDuration(c.EventWebhookRequestTimeout) + if err != nil { + fmt.Fprintln(os.Stderr, "parse event webhook request timeout: %w", err) + os.Exit(1) + } + + return result +} + // ParseProjectCacheTTL returns TTL for project metadata cache. func (c *Config) ParseProjectCacheTTL() time.Duration { result, err := time.ParseDuration(c.ProjectCacheTTL) diff --git a/server/backend/config_test.go b/server/backend/config_test.go index a7b3a34ba..cca8f4797 100644 --- a/server/backend/config_test.go +++ b/server/backend/config_test.go @@ -27,12 +27,15 @@ import ( func TestConfig(t *testing.T) { t.Run("validate test", func(t *testing.T) { validConf := backend.Config{ - ClientDeactivateThreshold: "1h", - AuthWebhookMaxWaitInterval: "0ms", - AuthWebhookMinWaitInterval: "0ms", - AuthWebhookRequestTimeout: "0ms", - AuthWebhookCacheTTL: "10s", - ProjectCacheTTL: "10m", + ClientDeactivateThreshold: "1h", + AuthWebhookMaxWaitInterval: "0ms", + AuthWebhookMinWaitInterval: "0ms", + AuthWebhookRequestTimeout: "0ms", + AuthWebhookCacheTTL: "10s", + ProjectCacheTTL: "10m", + EventWebhookMaxWaitInterval: "0ms", + EventWebhookMinWaitInterval: "0ms", + EventWebhookRequestTimeout: "0ms", } assert.NoError(t, validConf.Validate()) @@ -59,5 +62,17 @@ func TestConfig(t *testing.T) { conf6 := validConf conf6.ProjectCacheTTL = "10 minutes" assert.Error(t, conf6.Validate()) + + conf7 := validConf + conf7.EventWebhookMaxWaitInterval = "5" + assert.Error(t, conf7.Validate()) + + conf8 := validConf + conf8.EventWebhookMinWaitInterval = "3" + assert.Error(t, conf8.Validate()) + + conf9 := validConf + conf9.EventWebhookRequestTimeout = "1" + assert.Error(t, conf9.Validate()) }) } diff --git a/server/backend/database/project_info.go b/server/backend/database/project_info.go index ade8590d5..46d1ca881 100644 --- a/server/backend/database/project_info.go +++ b/server/backend/database/project_info.go @@ -57,6 +57,12 @@ type ProjectInfo struct { // AuthWebhookMethods is the methods that run the authorization webhook. AuthWebhookMethods []string `bson:"auth_webhook_methods"` + // EventWebhookURL is the URL of the event webhook. + EventWebhookURL string `bson:"event_webhook_url"` + + // EventWebhookEvents is the events that the event webhook listens to. + EventWebhookEvents []string `bson:"event_webhook_events"` + // ClientDeactivateThreshold is the time after which clients in // specific project are considered deactivate for housekeeping. ClientDeactivateThreshold string `bson:"client_deactivate_threshold"` @@ -94,6 +100,8 @@ func (i *ProjectInfo) DeepCopy() *ProjectInfo { SecretKey: i.SecretKey, AuthWebhookURL: i.AuthWebhookURL, AuthWebhookMethods: i.AuthWebhookMethods, + EventWebhookURL: i.EventWebhookURL, + EventWebhookEvents: i.EventWebhookEvents, ClientDeactivateThreshold: i.ClientDeactivateThreshold, CreatedAt: i.CreatedAt, UpdatedAt: i.UpdatedAt, @@ -111,6 +119,12 @@ func (i *ProjectInfo) UpdateFields(fields *types.UpdatableProjectFields) { if fields.AuthWebhookMethods != nil { i.AuthWebhookMethods = *fields.AuthWebhookMethods } + if fields.EventWebhookURL != nil { + i.EventWebhookURL = *fields.EventWebhookURL + } + if fields.EventWebhookEvents != nil { + i.EventWebhookEvents = *fields.EventWebhookEvents + } if fields.ClientDeactivateThreshold != nil { i.ClientDeactivateThreshold = *fields.ClientDeactivateThreshold } @@ -124,6 +138,8 @@ func (i *ProjectInfo) ToProject() *types.Project { Owner: i.Owner, AuthWebhookURL: i.AuthWebhookURL, AuthWebhookMethods: i.AuthWebhookMethods, + EventWebhookURL: i.EventWebhookURL, + EventWebhookEvents: i.EventWebhookEvents, ClientDeactivateThreshold: i.ClientDeactivateThreshold, PublicKey: i.PublicKey, SecretKey: i.SecretKey, diff --git a/server/backend/database/project_info_test.go b/server/backend/database/project_info_test.go index 658e2bf33..d3b21a63c 100644 --- a/server/backend/database/project_info_test.go +++ b/server/backend/database/project_info_test.go @@ -34,6 +34,7 @@ func TestProjectInfo(t *testing.T) { testName := "testName" testURL := "testUrl" testMethods := []string{"testMethod"} + testEvents := []string{"testEvent"} testClientDeactivateThreshold := "2h" project.UpdateFields(&types.UpdatableProjectFields{Name: &testName}) @@ -44,6 +45,13 @@ func TestProjectInfo(t *testing.T) { project.UpdateFields(&types.UpdatableProjectFields{AuthWebhookMethods: &testMethods}) assert.Equal(t, testMethods, project.AuthWebhookMethods) + + project.UpdateFields(&types.UpdatableProjectFields{EventWebhookURL: &testURL}) + assert.Equal(t, testURL, project.EventWebhookURL) + + project.UpdateFields(&types.UpdatableProjectFields{EventWebhookEvents: &testEvents}) + assert.Equal(t, testEvents, project.EventWebhookEvents) + assert.Equal(t, dummyOwnerID, project.Owner) project.UpdateFields(&types.UpdatableProjectFields{ diff --git a/server/backend/database/testcases/testcases.go b/server/backend/database/testcases/testcases.go index 3547534fa..d2962cb0a 100644 --- a/server/backend/database/testcases/testcases.go +++ b/server/backend/database/testcases/testcases.go @@ -767,6 +767,8 @@ func RunUpdateProjectInfoTest(t *testing.T, db database.Database) { string(types.AttachDocument), string(types.WatchDocuments), } + newEventWebhookURL := "http://localhost:4000" + newEventWebhookEvents := []string{string(types.DocRootChanged)} newClientDeactivateThreshold := "1h" info, err := db.CreateProjectInfo(ctx, t.Name(), dummyOwnerID, clientDeactivateThreshold) @@ -781,6 +783,8 @@ func RunUpdateProjectInfoTest(t *testing.T, db database.Database) { Name: &newName, AuthWebhookURL: &newAuthWebhookURL, AuthWebhookMethods: &newAuthWebhookMethods, + EventWebhookURL: &newEventWebhookURL, + EventWebhookEvents: &newEventWebhookEvents, ClientDeactivateThreshold: &newClientDeactivateThreshold, } assert.NoError(t, fields.Validate()) @@ -793,6 +797,8 @@ func RunUpdateProjectInfoTest(t *testing.T, db database.Database) { assert.Equal(t, newAuthWebhookURL, updateInfo.AuthWebhookURL) assert.Equal(t, newAuthWebhookMethods, updateInfo.AuthWebhookMethods) assert.Equal(t, newClientDeactivateThreshold, updateInfo.ClientDeactivateThreshold) + assert.Equal(t, newEventWebhookURL, updateInfo.EventWebhookURL) + assert.Equal(t, newEventWebhookEvents, updateInfo.EventWebhookEvents) // 02. Update name field test fields = &types.UpdatableProjectFields{ @@ -807,12 +813,16 @@ func RunUpdateProjectInfoTest(t *testing.T, db database.Database) { assert.NotEqual(t, newName, updateInfo.Name) assert.Equal(t, newAuthWebhookURL, updateInfo.AuthWebhookURL) assert.Equal(t, newAuthWebhookMethods, updateInfo.AuthWebhookMethods) + assert.Equal(t, newEventWebhookURL, updateInfo.EventWebhookURL) + assert.Equal(t, newEventWebhookEvents, updateInfo.EventWebhookEvents) assert.Equal(t, newClientDeactivateThreshold, updateInfo.ClientDeactivateThreshold) - // 03. Update authWebhookURL test + // 03. Update authWebhookURL and eventWebhookURL test + newEventWebhookURL2 := newEventWebhookURL + "2" newAuthWebhookURL2 := newAuthWebhookURL + "2" fields = &types.UpdatableProjectFields{ - AuthWebhookURL: &newAuthWebhookURL2, + EventWebhookURL: &newEventWebhookURL2, + AuthWebhookURL: &newAuthWebhookURL2, } assert.NoError(t, fields.Validate()) res, err = db.UpdateProjectInfo(ctx, dummyOwnerID, id, fields) @@ -822,10 +832,31 @@ func RunUpdateProjectInfoTest(t *testing.T, db database.Database) { assert.Equal(t, res, updateInfo) assert.Equal(t, newName2, updateInfo.Name) assert.NotEqual(t, newAuthWebhookURL, updateInfo.AuthWebhookURL) - assert.Equal(t, newAuthWebhookMethods, updateInfo.AuthWebhookMethods) + assert.Equal(t, newAuthWebhookURL2, updateInfo.AuthWebhookURL) + assert.NotEqual(t, newEventWebhookURL, updateInfo.EventWebhookURL) + assert.Equal(t, newEventWebhookURL2, updateInfo.EventWebhookURL) assert.Equal(t, newClientDeactivateThreshold, updateInfo.ClientDeactivateThreshold) - // 04. Update clientDeactivateThreshold test + // 04. Update EventWebhookEvents test + var newEventWebhookEvents2 []string + newAuthWebhookMethods2 := []string{ + string(types.DetachDocument), + string(types.PushPull), + } + fields = &types.UpdatableProjectFields{ + AuthWebhookMethods: &newAuthWebhookMethods2, + EventWebhookEvents: &newEventWebhookEvents2, + } + assert.NoError(t, fields.Validate()) + res, err = db.UpdateProjectInfo(ctx, dummyOwnerID, id, fields) + assert.NoError(t, err) + updateInfo, err = db.FindProjectInfoByID(ctx, id) + assert.NoError(t, err) + assert.Equal(t, res, updateInfo) + assert.Equal(t, newEventWebhookEvents2, updateInfo.EventWebhookEvents) + assert.Equal(t, newAuthWebhookMethods2, updateInfo.AuthWebhookMethods) + + // 05. Update clientDeactivateThreshold test clientDeactivateThreshold2 := "2h" fields = &types.UpdatableProjectFields{ ClientDeactivateThreshold: &clientDeactivateThreshold2, @@ -838,15 +869,17 @@ func RunUpdateProjectInfoTest(t *testing.T, db database.Database) { assert.Equal(t, res, updateInfo) assert.Equal(t, newName2, updateInfo.Name) assert.Equal(t, newAuthWebhookURL2, updateInfo.AuthWebhookURL) - assert.Equal(t, newAuthWebhookMethods, updateInfo.AuthWebhookMethods) + assert.Equal(t, newEventWebhookURL2, updateInfo.EventWebhookURL) + assert.Equal(t, newAuthWebhookMethods2, updateInfo.AuthWebhookMethods) + assert.Equal(t, newEventWebhookEvents2, updateInfo.EventWebhookEvents) assert.NotEqual(t, newClientDeactivateThreshold, updateInfo.ClientDeactivateThreshold) - // 05. Duplicated name test + // 06. Duplicated name test fields = &types.UpdatableProjectFields{Name: &existName} _, err = db.UpdateProjectInfo(ctx, dummyOwnerID, id, fields) assert.ErrorIs(t, err, database.ErrProjectNameAlreadyExists) - // 06. OwnerID not match test + // 07. OwnerID not match test fields = &types.UpdatableProjectFields{Name: &existName} _, err = db.UpdateProjectInfo(ctx, otherOwnerID, id, fields) assert.ErrorIs(t, err, database.ErrProjectNotFound) diff --git a/server/config.go b/server/config.go index fab17b578..68c14d563 100644 --- a/server/config.go +++ b/server/config.go @@ -70,8 +70,14 @@ const ( DefaultAuthWebhookMinWaitInterval = 100 * time.Millisecond DefaultAuthWebhookCacheSize = 5000 DefaultAuthWebhookCacheTTL = 10 * time.Second - DefaultProjectCacheSize = 256 - DefaultProjectCacheTTL = 10 * time.Minute + + DefaultEventWebhookRequestTimeout = 3 * time.Second + DefaultEventWebhookMaxRetries = 10 + DefaultEventWebhookMaxWaitInterval = 3000 * time.Millisecond + DefaultEventWebhookMinWaitInterval = 100 * time.Millisecond + + DefaultProjectCacheSize = 256 + DefaultProjectCacheTTL = 10 * time.Minute DefaultHostname = "" DefaultGatewayAddr = "localhost:8080" @@ -210,6 +216,22 @@ func (c *Config) ensureDefaultValue() { c.Backend.AuthWebhookCacheTTL = DefaultAuthWebhookCacheTTL.String() } + if c.Backend.EventWebhookMaxRetries == 0 { + c.Backend.EventWebhookMaxRetries = DefaultEventWebhookMaxRetries + } + + if c.Backend.EventWebhookMaxWaitInterval == "" { + c.Backend.EventWebhookMaxWaitInterval = DefaultEventWebhookMaxWaitInterval.String() + } + + if c.Backend.EventWebhookMinWaitInterval == "" { + c.Backend.EventWebhookMinWaitInterval = DefaultEventWebhookMinWaitInterval.String() + } + + if c.Backend.EventWebhookRequestTimeout == "" { + c.Backend.EventWebhookRequestTimeout = DefaultEventWebhookRequestTimeout.String() + } + if c.Backend.ProjectCacheSize == 0 { c.Backend.ProjectCacheSize = DefaultProjectCacheSize } diff --git a/server/config.sample.yml b/server/config.sample.yml index eca070518..3b94c11c4 100644 --- a/server/config.sample.yml +++ b/server/config.sample.yml @@ -80,6 +80,18 @@ Backend: # AuthWebhookCacheTTL is the TTL value to set when caching the authorized result. AuthWebhookCacheTTL: "10s" + # EventWebhookRequestTimeout is the timeout for each event webhook request. + EventWebhookRequestTimeout: "3s" + + # EventWebhookMaxRetries is the max number of retries for the event webhook. + EventWebhookMaxRetries: 10 + + # EventWebhookMinWaitInterval is the minimum wait interval between retries(exponential backoff). + EventWebhookMinWaitInterval: "100ms" + + # EventWebhookMaxWaitInterval is the maximum wait interval between retries(exponential backoff). + EventWebhookMaxWaitInterval: "3s" + # ProjectCacheSize is the size of the project metadata cache. ProjectCacheSize: 256 diff --git a/server/config_test.go b/server/config_test.go index d4b83ee41..5746065c1 100644 --- a/server/config_test.go +++ b/server/config_test.go @@ -61,6 +61,7 @@ func TestNewConfigFromFile(t *testing.T) { assert.Equal(t, conf.Backend.SnapshotThreshold, int64(server.DefaultSnapshotThreshold)) assert.Equal(t, conf.Backend.SnapshotInterval, int64(server.DefaultSnapshotInterval)) assert.Equal(t, conf.Backend.AuthWebhookMaxRetries, uint64(server.DefaultAuthWebhookMaxRetries)) + assert.Equal(t, conf.Backend.EventWebhookMaxRetries, uint64(server.DefaultEventWebhookMaxRetries)) ClientDeactivateThreshold := conf.Backend.ClientDeactivateThreshold assert.NoError(t, err) @@ -82,6 +83,18 @@ func TestNewConfigFromFile(t *testing.T) { assert.NoError(t, err) assert.Equal(t, authWebhookCacheTTL, server.DefaultAuthWebhookCacheTTL) + eventWebhookMaxWaitInterval, err := time.ParseDuration(conf.Backend.EventWebhookMaxWaitInterval) + assert.NoError(t, err) + assert.Equal(t, eventWebhookMaxWaitInterval, server.DefaultEventWebhookMaxWaitInterval) + + eventWebhookMinWaitInterval, err := time.ParseDuration(conf.Backend.EventWebhookMinWaitInterval) + assert.NoError(t, err) + assert.Equal(t, eventWebhookMinWaitInterval, server.DefaultEventWebhookMinWaitInterval) + + eventWebhookRequestTimeout, err := time.ParseDuration(conf.Backend.EventWebhookRequestTimeout) + assert.NoError(t, err) + assert.Equal(t, eventWebhookRequestTimeout, server.DefaultEventWebhookRequestTimeout) + projectCacheTTL, err := time.ParseDuration(conf.Backend.ProjectCacheTTL) assert.NoError(t, err) assert.Equal(t, projectCacheTTL, server.DefaultProjectCacheTTL) diff --git a/server/packs/packs.go b/server/packs/packs.go index 7a80130ab..c891fc9e6 100644 --- a/server/packs/packs.go +++ b/server/packs/packs.go @@ -37,6 +37,7 @@ import ( "github.com/yorkie-team/yorkie/server/backend/database" "github.com/yorkie-team/yorkie/server/backend/sync" "github.com/yorkie-team/yorkie/server/logging" + "github.com/yorkie-team/yorkie/server/webhook" ) // PushPullKey creates a new sync.Key of PushPull for the given document. @@ -185,6 +186,9 @@ func PushPull( return } + // TODO(hackerwins): For now, we are publishing the event to pubsub and + // webhook manually. But we need to consider unified event handling system + // to handle this with rate-limiter and retry mechanism. be.PubSub.Publish( ctx, publisherID, @@ -195,6 +199,19 @@ func PushPull( }, ) + if reqPack.OperationsLen() > 0 { + if err := webhook.SendEvent( + ctx, + be, + project, + docInfo.Key.String(), + events.DocRootChangedEvent, + ); err != nil { + logging.From(ctx).Error(err) + return + } + } + locker, err := be.Locker.NewLocker(ctx, SnapshotKey(project.ID, reqPack.DocumentKey)) if err != nil { logging.From(ctx).Error(err) diff --git a/server/packs/packs_test.go b/server/packs/packs_test.go index 6de23f1cc..5b964008e 100644 --- a/server/packs/packs_test.go +++ b/server/packs/packs_test.go @@ -94,19 +94,22 @@ func TestMain(m *testing.M) { testBackend, err = backend.New( &backend.Config{ - AdminUser: helper.AdminUser, - AdminPassword: helper.AdminPassword, - UseDefaultProject: helper.UseDefaultProject, - ClientDeactivateThreshold: helper.ClientDeactivateThreshold, - SnapshotThreshold: helper.SnapshotThreshold, - AuthWebhookCacheSize: helper.AuthWebhookSize, - AuthWebhookCacheTTL: helper.AuthWebhookCacheTTL.String(), - AuthWebhookMaxWaitInterval: helper.AuthWebhookMaxWaitInterval.String(), - AuthWebhookMinWaitInterval: helper.AuthWebhookMinWaitInterval.String(), - AuthWebhookRequestTimeout: helper.AuthWebhookRequestTimeout.String(), - ProjectCacheSize: helper.ProjectCacheSize, - ProjectCacheTTL: helper.ProjectCacheTTL.String(), - AdminTokenDuration: helper.AdminTokenDuration, + AdminUser: helper.AdminUser, + AdminPassword: helper.AdminPassword, + UseDefaultProject: helper.UseDefaultProject, + ClientDeactivateThreshold: helper.ClientDeactivateThreshold, + SnapshotThreshold: helper.SnapshotThreshold, + AuthWebhookCacheSize: helper.AuthWebhookSize, + AuthWebhookCacheTTL: helper.AuthWebhookCacheTTL.String(), + AuthWebhookMaxWaitInterval: helper.AuthWebhookMaxWaitInterval.String(), + AuthWebhookMinWaitInterval: helper.AuthWebhookMinWaitInterval.String(), + AuthWebhookRequestTimeout: helper.AuthWebhookRequestTimeout.String(), + EventWebhookMaxWaitInterval: helper.EventWebhookMaxWaitInterval.String(), + EventWebhookMinWaitInterval: helper.EventWebhookMinWaitInterval.String(), + EventWebhookRequestTimeout: helper.EventWebhookRequestTimeout.String(), + ProjectCacheSize: helper.ProjectCacheSize, + ProjectCacheTTL: helper.ProjectCacheTTL.String(), + AdminTokenDuration: helper.AdminTokenDuration, }, &mongo.Config{ ConnectionURI: helper.MongoConnectionURI, YorkieDatabase: helper.TestDBName(), diff --git a/server/rpc/auth/webhook.go b/server/rpc/auth/webhook.go index cb27e5df3..e4bb68b9d 100644 --- a/server/rpc/auth/webhook.go +++ b/server/rpc/auth/webhook.go @@ -58,11 +58,11 @@ func verifyAccess( } cacheKey := generateCacheKey(prj.PublicKey, body) - if entry, ok := be.WebhookCache.Get(cacheKey); ok { + if entry, ok := be.AuthWebhookCache.Get(cacheKey); ok { return handleWebhookResponse(entry.First, entry.Second) } - res, status, err := be.WebhookClient.Send( + res, status, err := be.AuthWebhookClient.Send( ctx, prj.AuthWebhookURL, "", @@ -74,7 +74,7 @@ func verifyAccess( // TODO(hackerwins): We should consider caching the response of Unauthorized as well. if status != http.StatusUnauthorized { - be.WebhookCache.Add( + be.AuthWebhookCache.Add( cacheKey, pkgtypes.Pair[int, *types.AuthWebhookResponse]{First: status, Second: res}, be.Config.ParseAuthWebhookCacheTTL(), diff --git a/server/rpc/server_test.go b/server/rpc/server_test.go index 55360246d..23052545a 100644 --- a/server/rpc/server_test.go +++ b/server/rpc/server_test.go @@ -68,19 +68,22 @@ func TestMain(m *testing.M) { } be, err := backend.New(&backend.Config{ - AdminUser: helper.AdminUser, - AdminPassword: helper.AdminPassword, - UseDefaultProject: helper.UseDefaultProject, - ClientDeactivateThreshold: helper.ClientDeactivateThreshold, - SnapshotThreshold: helper.SnapshotThreshold, - AuthWebhookCacheSize: helper.AuthWebhookSize, - AuthWebhookCacheTTL: helper.AuthWebhookCacheTTL.String(), - AuthWebhookMaxWaitInterval: helper.AuthWebhookMaxWaitInterval.String(), - AuthWebhookMinWaitInterval: helper.AuthWebhookMinWaitInterval.String(), - AuthWebhookRequestTimeout: helper.AuthWebhookRequestTimeout.String(), - ProjectCacheSize: helper.ProjectCacheSize, - ProjectCacheTTL: helper.ProjectCacheTTL.String(), - AdminTokenDuration: helper.AdminTokenDuration, + AdminUser: helper.AdminUser, + AdminPassword: helper.AdminPassword, + UseDefaultProject: helper.UseDefaultProject, + ClientDeactivateThreshold: helper.ClientDeactivateThreshold, + SnapshotThreshold: helper.SnapshotThreshold, + AuthWebhookCacheSize: helper.AuthWebhookSize, + AuthWebhookCacheTTL: helper.AuthWebhookCacheTTL.String(), + AuthWebhookMaxWaitInterval: helper.AuthWebhookMaxWaitInterval.String(), + AuthWebhookMinWaitInterval: helper.AuthWebhookMinWaitInterval.String(), + AuthWebhookRequestTimeout: helper.AuthWebhookRequestTimeout.String(), + EventWebhookMaxWaitInterval: helper.EventWebhookMaxWaitInterval.String(), + EventWebhookMinWaitInterval: helper.EventWebhookMinWaitInterval.String(), + EventWebhookRequestTimeout: helper.EventWebhookRequestTimeout.String(), + ProjectCacheSize: helper.ProjectCacheSize, + ProjectCacheTTL: helper.ProjectCacheTTL.String(), + AdminTokenDuration: helper.AdminTokenDuration, }, &mongo.Config{ ConnectionURI: helper.MongoConnectionURI, YorkieDatabase: helper.TestDBName(), diff --git a/server/webhook/events.go b/server/webhook/events.go new file mode 100644 index 000000000..76c007373 --- /dev/null +++ b/server/webhook/events.go @@ -0,0 +1,92 @@ +/* + * 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 webhook provides publishing events to project endpoint. +package webhook + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + gotime "time" + + "github.com/yorkie-team/yorkie/api/types" + "github.com/yorkie-team/yorkie/api/types/events" + "github.com/yorkie-team/yorkie/server/backend" +) + +var ( + // ErrUnexpectedStatusCode is returned when the webhook returns an unexpected status code. + ErrUnexpectedStatusCode = errors.New("unexpected status code from webhook") +) + +// SendEvent sends an event to the project's event webhook endpoint. +func SendEvent( + ctx context.Context, + be *backend.Backend, + prj *types.Project, + docKey string, + eventType events.DocEventType, +) error { + webhookType := eventType.WebhookType() + if webhookType == "" { + return fmt.Errorf("invalid event webhook type: %s", eventType) + } + + if !prj.RequireEventWebhook(webhookType) { + return nil + } + + body, err := buildRequestBody(docKey, webhookType) + if err != nil { + return fmt.Errorf("marshal event webhook request: %w", err) + } + + _, status, err := be.EventWebhookClient.Send( + ctx, + prj.EventWebhookURL, + prj.SecretKey, + body, + ) + if err != nil { + return fmt.Errorf("send event webhook: %w", err) + } + if status != http.StatusOK { + return fmt.Errorf("send event webhook %d: %w", status, ErrUnexpectedStatusCode) + } + + return nil +} + +// buildRequestBody builds the request body for the event webhook. +func buildRequestBody(docKey string, webhookType types.EventWebhookType) ([]byte, error) { + req := types.EventWebhookRequest{ + Type: webhookType, + Attributes: types.EventWebhookAttribute{ + Key: docKey, + IssuedAt: gotime.Now().UTC().Format("2006-01-02T15:04:05.000Z"), + }, + } + + body, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("marshal event webhook request: %w", err) + } + + return body, nil +} diff --git a/test/bench/grpc_bench_test.go b/test/bench/grpc_bench_test.go index bdfa36feb..c5850b6f5 100644 --- a/test/bench/grpc_bench_test.go +++ b/test/bench/grpc_bench_test.go @@ -99,6 +99,8 @@ func benchmarkUpdateProject(ctx context.Context, b *testing.B, cnt int, adminCli for _, m := range types.AuthMethods() { authWebhookMethods = append(authWebhookMethods, string(m)) } + eventWebhookURL := fmt.Sprintf("http://eventWebhookURL%d", i) + eventWebhookEvents := []string{string(types.DocRootChanged)} clientDeactivateThreshold := "1h" _, err := adminCli.UpdateProject( @@ -108,6 +110,8 @@ func benchmarkUpdateProject(ctx context.Context, b *testing.B, cnt int, adminCli Name: &name, AuthWebhookURL: &authWebhookURL, AuthWebhookMethods: &authWebhookMethods, + EventWebhookURL: &eventWebhookURL, + EventWebhookEvents: &eventWebhookEvents, ClientDeactivateThreshold: &clientDeactivateThreshold, }, ) diff --git a/test/complex/main_test.go b/test/complex/main_test.go index a32c35601..57dac7a52 100644 --- a/test/complex/main_test.go +++ b/test/complex/main_test.go @@ -73,20 +73,23 @@ func TestMain(m *testing.M) { } be, err := backend.New(&backend.Config{ - AdminUser: helper.AdminUser, - AdminPassword: helper.AdminPassword, - UseDefaultProject: helper.UseDefaultProject, - ClientDeactivateThreshold: helper.ClientDeactivateThreshold, - SnapshotThreshold: helper.SnapshotThreshold, - AuthWebhookMaxWaitInterval: helper.AuthWebhookMaxWaitInterval.String(), - AuthWebhookMinWaitInterval: helper.AuthWebhookMinWaitInterval.String(), - AuthWebhookRequestTimeout: helper.AuthWebhookRequestTimeout.String(), - AuthWebhookCacheSize: helper.AuthWebhookSize, - AuthWebhookCacheTTL: helper.AuthWebhookCacheTTL.String(), - ProjectCacheSize: helper.ProjectCacheSize, - ProjectCacheTTL: helper.ProjectCacheTTL.String(), - AdminTokenDuration: helper.AdminTokenDuration, - GatewayAddr: fmt.Sprintf("localhost:%d", helper.RPCPort), + AdminUser: helper.AdminUser, + AdminPassword: helper.AdminPassword, + UseDefaultProject: helper.UseDefaultProject, + ClientDeactivateThreshold: helper.ClientDeactivateThreshold, + SnapshotThreshold: helper.SnapshotThreshold, + AuthWebhookMaxWaitInterval: helper.AuthWebhookMaxWaitInterval.String(), + AuthWebhookMinWaitInterval: helper.AuthWebhookMinWaitInterval.String(), + AuthWebhookRequestTimeout: helper.AuthWebhookRequestTimeout.String(), + AuthWebhookCacheSize: helper.AuthWebhookSize, + AuthWebhookCacheTTL: helper.AuthWebhookCacheTTL.String(), + EventWebhookMaxWaitInterval: helper.EventWebhookMaxWaitInterval.String(), + EventWebhookMinWaitInterval: helper.EventWebhookMinWaitInterval.String(), + EventWebhookRequestTimeout: helper.EventWebhookRequestTimeout.String(), + ProjectCacheSize: helper.ProjectCacheSize, + ProjectCacheTTL: helper.ProjectCacheTTL.String(), + AdminTokenDuration: helper.AdminTokenDuration, + GatewayAddr: fmt.Sprintf("localhost:%d", helper.RPCPort), }, &mongo.Config{ ConnectionURI: helper.MongoConnectionURI, YorkieDatabase: shardedDBNameForServer, diff --git a/test/helper/helper.go b/test/helper/helper.go index 27bd31cbd..4e21eafbd 100644 --- a/test/helper/helper.go +++ b/test/helper/helper.go @@ -70,17 +70,22 @@ var ( HousekeepingCandidatesLimitPerProject = 10 HousekeepingProjectFetchSize = 10 - AdminTokenDuration = "10s" - ClientDeactivateThreshold = "10s" - SnapshotThreshold = int64(10) - SnapshotWithPurgingChanges = false - AuthWebhookMaxWaitInterval = 3 * gotime.Millisecond - AuthWebhookMinWaitInterval = 3 * gotime.Millisecond - AuthWebhookRequestTimeout = 100 * gotime.Millisecond - AuthWebhookSize = 100 - AuthWebhookCacheTTL = 10 * gotime.Second - ProjectCacheSize = 256 - ProjectCacheTTL = 5 * gotime.Second + AdminTokenDuration = "10s" + ClientDeactivateThreshold = "10s" + SnapshotThreshold = int64(10) + SnapshotWithPurgingChanges = false + AuthWebhookMaxWaitInterval = 3 * gotime.Millisecond + AuthWebhookMinWaitInterval = 3 * gotime.Millisecond + AuthWebhookRequestTimeout = 100 * gotime.Millisecond + AuthWebhookSize = 100 + AuthWebhookCacheTTL = 10 * gotime.Second + EventWebhookMaxWaitInterval = 3 * gotime.Millisecond + EventWebhookMinWaitInterval = 3 * gotime.Millisecond + EventWebhookRequestTimeout = 100 * gotime.Millisecond + EventWebhookSize = 100 + EventWebhookCacheTTL = 10 * gotime.Second + ProjectCacheSize = 256 + ProjectCacheTTL = 5 * gotime.Second MongoConnectionURI = "mongodb://localhost:27017" MongoConnectionTimeout = "5s" @@ -265,23 +270,26 @@ func TestConfig() *server.Config { ProjectFetchSize: HousekeepingProjectFetchSize, }, Backend: &backend.Config{ - AdminUser: server.DefaultAdminUser, - AdminPassword: server.DefaultAdminPassword, - SecretKey: server.DefaultSecretKey, - AdminTokenDuration: server.DefaultAdminTokenDuration.String(), - UseDefaultProject: true, - ClientDeactivateThreshold: server.DefaultClientDeactivateThreshold, - SnapshotInterval: 10, - SnapshotThreshold: SnapshotThreshold, - SnapshotWithPurgingChanges: SnapshotWithPurgingChanges, - AuthWebhookMaxWaitInterval: AuthWebhookMaxWaitInterval.String(), - AuthWebhookMinWaitInterval: AuthWebhookMinWaitInterval.String(), - AuthWebhookRequestTimeout: AuthWebhookRequestTimeout.String(), - AuthWebhookCacheSize: AuthWebhookSize, - AuthWebhookCacheTTL: AuthWebhookCacheTTL.String(), - ProjectCacheSize: ProjectCacheSize, - ProjectCacheTTL: ProjectCacheTTL.String(), - GatewayAddr: fmt.Sprintf("localhost:%d", RPCPort+portOffset), + AdminUser: server.DefaultAdminUser, + AdminPassword: server.DefaultAdminPassword, + SecretKey: server.DefaultSecretKey, + AdminTokenDuration: server.DefaultAdminTokenDuration.String(), + UseDefaultProject: true, + ClientDeactivateThreshold: server.DefaultClientDeactivateThreshold, + SnapshotInterval: 10, + SnapshotThreshold: SnapshotThreshold, + SnapshotWithPurgingChanges: SnapshotWithPurgingChanges, + AuthWebhookMaxWaitInterval: AuthWebhookMaxWaitInterval.String(), + AuthWebhookMinWaitInterval: AuthWebhookMinWaitInterval.String(), + AuthWebhookRequestTimeout: AuthWebhookRequestTimeout.String(), + AuthWebhookCacheSize: AuthWebhookSize, + AuthWebhookCacheTTL: AuthWebhookCacheTTL.String(), + EventWebhookMaxWaitInterval: EventWebhookMaxWaitInterval.String(), + EventWebhookMinWaitInterval: EventWebhookMinWaitInterval.String(), + EventWebhookRequestTimeout: EventWebhookRequestTimeout.String(), + ProjectCacheSize: ProjectCacheSize, + ProjectCacheTTL: ProjectCacheTTL.String(), + GatewayAddr: fmt.Sprintf("localhost:%d", RPCPort+portOffset), }, Mongo: &mongo.Config{ ConnectionURI: MongoConnectionURI, diff --git a/test/integration/event_webhook_test.go b/test/integration/event_webhook_test.go new file mode 100644 index 000000000..50e932727 --- /dev/null +++ b/test/integration/event_webhook_test.go @@ -0,0 +1,300 @@ +//go:build integration + +/* + * 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 integration + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + gojson "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/yorkie-team/yorkie/api/types" + "github.com/yorkie-team/yorkie/client" + "github.com/yorkie-team/yorkie/pkg/document" + "github.com/yorkie-team/yorkie/pkg/document/crdt" + "github.com/yorkie-team/yorkie/pkg/document/json" + "github.com/yorkie-team/yorkie/pkg/document/presence" + "github.com/yorkie-team/yorkie/server" + "github.com/yorkie-team/yorkie/test/helper" +) + +func verifySignature(signatureHeader, secret string, body []byte) error { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write(body) + expectedSig := hex.EncodeToString(mac.Sum(nil)) + expectedSigHeader := fmt.Sprintf("sha256=%s", expectedSig) + if !hmac.Equal([]byte(signatureHeader), []byte(expectedSigHeader)) { + return errors.New("signature validation failed") + } + return nil +} + +func newWebhookServer(t *testing.T, secretKey, docKey string) (*httptest.Server, *int32) { + var reqCnt int32 + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&reqCnt, 1) + signatureHeader := r.Header.Get("X-Signature-256") + assert.NotZero(t, len(signatureHeader)) + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.NoError(t, verifySignature(signatureHeader, secretKey, body)) + + req := &types.EventWebhookRequest{} + assert.NoError(t, gojson.Unmarshal(body, req)) + assert.Equal(t, types.DocRootChanged, req.Type) + assert.Equal(t, docKey, req.Attributes.Key) + + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(func() { srv.Close() }) + + return srv, &reqCnt +} + +// newYorkieServer initializes the Yorkie server and admin client. +func newYorkieServer(t *testing.T, projectCacheTTL string) *server.Yorkie { + conf := helper.TestConfig() + if projectCacheTTL != "default" { + conf.Backend.ProjectCacheTTL = projectCacheTTL + } + svr, err := server.New(conf) + assert.NoError(t, err) + assert.NoError(t, svr.Start()) + + t.Cleanup(func() { + assert.NoError(t, svr.Shutdown(true)) + }) + + return svr +} + +func newActivatedClient(t *testing.T, ctx context.Context, addr, publicKey string) *client.Client { + cli, err := client.Dial(addr, client.WithAPIKey(publicKey)) + assert.NoError(t, err) + assert.NoError(t, cli.Activate(ctx)) + t.Cleanup(func() { + assert.NoError(t, cli.Deactivate(ctx)) + assert.NoError(t, cli.Close()) + }) + return cli +} + +func TestRegisterEventWebhook(t *testing.T) { + ctx := context.Background() + + // Set up yorkie server + projectCacheTTL := 1 * time.Millisecond + svr := newYorkieServer(t, projectCacheTTL.String()) + + // Set up project + adminCli := helper.CreateAdminCli(t, svr.RPCAddr()) + defer func() { adminCli.Close() }() + + project, err := adminCli.CreateProject(ctx, "register-event-webhook") + assert.NoError(t, err) + + doc := document.New(helper.TestDocKey(t)) + userServer, getReqCnt := newWebhookServer(t, project.SecretKey, doc.Key().String()) + + cli := newActivatedClient(t, ctx, svr.RPCAddr(), project.PublicKey) + + assert.NoError(t, cli.Attach(ctx, doc, client.WithInitialRoot(map[string]any{ + "counter": json.NewCounter(0, crdt.LongCnt), + }))) + + waitWebhookReceived := 10 * time.Millisecond + + t.Run("register event webhook test", func(t *testing.T) { + // 01. Register event webhook + prj, err := adminCli.UpdateProject(ctx, project.ID.String(), &types.UpdatableProjectFields{ + EventWebhookURL: &userServer.URL, + EventWebhookEvents: &[]string{string(types.DocRootChanged)}, + }) + assert.NoError(t, err) + assert.Equal(t, userServer.URL, prj.EventWebhookURL) + assert.Equal(t, string(types.DocRootChanged), prj.EventWebhookEvents[0]) + + // 02. Wait project cache expired + time.Sleep(projectCacheTTL) + + // 03. Check webhook received + prev := atomic.LoadInt32(getReqCnt) + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + root.GetCounter("counter").Increase(1) + return nil + })) + assert.NoError(t, cli.Sync(ctx)) + time.Sleep(waitWebhookReceived) + assert.Equal(t, prev+1, atomic.LoadInt32(getReqCnt)) + }) + + t.Run("unregister event webhook test", func(t *testing.T) { + // 01. Unregister event webhook + prj, err := adminCli.UpdateProject(ctx, project.ID.String(), &types.UpdatableProjectFields{ + EventWebhookURL: &userServer.URL, + EventWebhookEvents: &[]string{}, + }) + assert.NoError(t, err) + assert.Equal(t, userServer.URL, prj.EventWebhookURL) + assert.Equal(t, 0, len(prj.EventWebhookEvents)) + + // 02. Wait project cache expired + time.Sleep(projectCacheTTL) + + // 03. Check webhook doesn't trigger + prev := atomic.LoadInt32(getReqCnt) + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + root.GetCounter("counter").Increase(1) + return nil + })) + assert.NoError(t, cli.Sync(ctx)) + + // 04. Wait webhook received + time.Sleep(waitWebhookReceived) + assert.Equal(t, prev, atomic.LoadInt32(getReqCnt)) + }) +} + +func TestDocRootChangedEventWebhook(t *testing.T) { + ctx := context.Background() + + svr := newYorkieServer(t, "default") + adminCli := helper.CreateAdminCli(t, svr.RPCAddr()) + + project, err := adminCli.CreateProject(ctx, "doc-root-changed-event-webhook") + assert.NoError(t, err) + + doc := document.New(helper.TestDocKey(t)) + userServer, getReqCnt := newWebhookServer(t, project.SecretKey, doc.Key().String()) + + project.EventWebhookURL = userServer.URL + _, err = adminCli.UpdateProject(ctx, project.ID.String(), &types.UpdatableProjectFields{ + EventWebhookURL: &project.EventWebhookURL, + EventWebhookEvents: &[]string{string(types.DocRootChanged)}, + }) + assert.NoError(t, err) + + cli := newActivatedClient(t, ctx, svr.RPCAddr(), project.PublicKey) + + assert.NoError(t, cli.Attach(ctx, doc, client.WithInitialRoot(map[string]any{ + "counter": json.NewCounter(0, crdt.LongCnt), + }))) + + waitWebhookReceived := 10 * time.Millisecond + t.Run("root element changed test", func(t *testing.T) { + prev := atomic.LoadInt32(getReqCnt) + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + root.GetCounter("counter").Increase(1) + return nil + })) + assert.NoError(t, cli.Sync(ctx)) + time.Sleep(waitWebhookReceived) + assert.Equal(t, prev+1, atomic.LoadInt32(getReqCnt)) + }) + + t.Run("presence changed test", func(t *testing.T) { + prev := atomic.LoadInt32(getReqCnt) + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + p.Set("update", "2") + return nil + })) + assert.NoError(t, cli.Sync(ctx)) + time.Sleep(waitWebhookReceived) + assert.Equal(t, prev, atomic.LoadInt32(getReqCnt)) + }) + + t.Run("root element and presence changed test", func(t *testing.T) { + prev := atomic.LoadInt32(getReqCnt) + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + p.Set("update", "3") + root.GetCounter("counter").Increase(1) + return nil + })) + assert.NoError(t, cli.Sync(ctx)) + time.Sleep(waitWebhookReceived) + assert.Equal(t, prev+1, atomic.LoadInt32(getReqCnt)) + }) +} + +func TestEventWebhookCache(t *testing.T) { + ctx := context.Background() + + webhookCacheTTL := 10 * time.Millisecond + svr := newYorkieServer(t, "default") + adminCli := helper.CreateAdminCli(t, svr.RPCAddr()) + + project, err := adminCli.CreateProject(ctx, "event-webhook-cache-webhook") + assert.NoError(t, err) + + doc := document.New(helper.TestDocKey(t)) + userServer, getReqCnt := newWebhookServer(t, project.SecretKey, doc.Key().String()) + _, err = adminCli.UpdateProject(ctx, project.ID.String(), &types.UpdatableProjectFields{ + EventWebhookURL: &userServer.URL, + EventWebhookEvents: &[]string{string(types.DocRootChanged)}, + }) + assert.NoError(t, err) + + cli := newActivatedClient(t, ctx, svr.RPCAddr(), project.PublicKey) + assert.NoError(t, cli.Attach(ctx, doc, client.WithInitialRoot(map[string]any{ + "counter": json.NewCounter(0, crdt.LongCnt), + }))) + + waitWebhookReceived := 20 * time.Millisecond + t.Run("throttling event test", func(t *testing.T) { + t.Skip("remove this after implement advanced event timing control") + + expectedUpdates := 5 + testDuration := webhookCacheTTL * time.Duration(expectedUpdates) + interval := webhookCacheTTL / 10 + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + timeCtx, cancel := context.WithTimeout(ctx, testDuration) + defer cancel() + + prevCnt := atomic.LoadInt32(getReqCnt) + for { + select { + case <-ticker.C: + assert.NoError(t, doc.Update(func(root *json.Object, p *presence.Presence) error { + root.GetCounter("counter").Increase(1) + return nil + })) + assert.NoError(t, cli.Sync(ctx)) + case <-timeCtx.Done(): + time.Sleep(waitWebhookReceived) + assert.Equal(t, prevCnt+int32(expectedUpdates), atomic.LoadInt32(getReqCnt)) + return + } + } + }) +}