bullet_train-outgoing_webhooks 1.0.2 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/app/assets/config/{bullet_train_outgoing_webhooks_web_manifest.js → bullet_train_outgoing_webhooks_manifest.js} +0 -0
  4. data/app/controllers/account/webhooks/outgoing/endpoints_controller.rb +4 -3
  5. data/app/controllers/api/v1/webhooks/outgoing/deliveries_endpoint.rb +63 -0
  6. data/app/controllers/api/v1/webhooks/outgoing/delivery_attempts_endpoint.rb +65 -0
  7. data/app/controllers/api/v1/webhooks/outgoing/endpoints_endpoint.rb +119 -0
  8. data/app/jobs/webhooks/outgoing/delivery_job.rb +7 -0
  9. data/app/jobs/webhooks/outgoing/generate_job.rb +7 -0
  10. data/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb +64 -0
  11. data/app/models/concerns/webhooks/outgoing/delivery_support.rb +66 -0
  12. data/app/models/concerns/webhooks/outgoing/endpoint_support.rb +37 -0
  13. data/app/models/concerns/webhooks/outgoing/event_support.rb +45 -0
  14. data/app/models/concerns/webhooks/outgoing/issuing_model.rb +67 -0
  15. data/app/models/concerns/webhooks/outgoing/team_support.rb +31 -0
  16. data/app/models/concerns/webhooks/outgoing/uri_filtering.rb +149 -0
  17. data/app/models/webhooks/outgoing/delivery.rb +20 -0
  18. data/app/models/webhooks/outgoing/delivery_attempt.rb +20 -0
  19. data/app/models/webhooks/outgoing/endpoint.rb +20 -0
  20. data/app/models/webhooks/outgoing/event.rb +3 -0
  21. data/app/models/webhooks/outgoing/event_type.rb +13 -0
  22. data/app/models/webhooks/outgoing.rb +5 -0
  23. data/app/models/webhooks.rb +5 -0
  24. data/app/serializers/api/v1/webhooks/outgoing/delivery_attempt_serializer.rb +16 -0
  25. data/app/serializers/api/v1/webhooks/outgoing/delivery_serializer.rb +14 -0
  26. data/app/serializers/api/v1/webhooks/outgoing/endpoint_serializer.rb +14 -0
  27. data/app/views/account/webhooks/outgoing/deliveries/_menu_item.html.erb +2 -2
  28. data/app/views/account/webhooks/outgoing/delivery_attempts/_menu_item.html.erb +2 -2
  29. data/app/views/account/webhooks/outgoing/endpoints/_breadcrumbs.html.erb +1 -1
  30. data/app/views/account/webhooks/outgoing/endpoints/_form.html.erb +2 -2
  31. data/app/views/account/webhooks/outgoing/endpoints/_index.html.erb +3 -3
  32. data/app/views/account/webhooks/outgoing/endpoints/_menu_item.html.erb +2 -2
  33. data/app/views/account/webhooks/outgoing/endpoints/show.html.erb +1 -1
  34. data/config/routes.rb +1 -1
  35. data/db/migrate/20180420013127_create_webhooks_outgoing_endpoints.rb +10 -0
  36. data/db/migrate/20180420013505_create_webhooks_outgoing_endpoint_event_types.rb +10 -0
  37. data/db/migrate/20180420013852_create_webhooks_outgoing_event_types.rb +9 -0
  38. data/db/migrate/20180420014623_create_webhooks_outgoing_events.rb +12 -0
  39. data/db/migrate/20180420021016_create_webhooks_outgoing_deliveries.rb +11 -0
  40. data/db/migrate/20180420022027_add_team_to_webhooks_outgoing_events.rb +5 -0
  41. data/db/migrate/20180420165345_create_webhooks_outgoing_delivery_attempts.rb +11 -0
  42. data/db/migrate/20180420172112_add_error_message_to_webhooks_outgoing_delivery_attempt.rb +6 -0
  43. data/db/migrate/20181012234232_add_name_to_webhooks_outgoing_endpoints.rb +5 -0
  44. data/db/migrate/20181013030208_add_delivered_at_to_webhooks_outgoing_deliveries.rb +5 -0
  45. data/db/migrate/20181013165056_add_uuid_to_webhooks_outgoing_events.rb +5 -0
  46. data/db/migrate/20181013174539_add_payload_to_webhooks_outgoing_events.rb +5 -0
  47. data/db/migrate/20181013192951_add_attempt_number_to_webhooks_outgoing_delivery_attempts.rb +5 -0
  48. data/db/migrate/20210805060451_change_body_to_data_on_webhooks_outgoing_events.rb +5 -0
  49. data/db/migrate/20211126230846_add_event_type_ids_to_webhooks_outgoing_endpoints.rb +5 -0
  50. data/db/migrate/20211126230847_migrate_event_type_ids_on_webhooks_outgoing_endpoints.rb +8 -0
  51. data/db/migrate/20211127013539_remove_event_type_from_webhooks_outgoing_events.rb +5 -0
  52. data/db/migrate/20211127013800_add_new_event_type_id_to_webhooks_outgoing_events.rb +5 -0
  53. data/db/migrate/20211127014001_migrate_event_types_on_webhooks_outgoing_events.rb +10 -0
  54. data/db/migrate/20211127015712_drop_webhooks_outgoing_endpoint_event_types.rb +9 -0
  55. data/db/migrate/20211127015713_drop_webhooks_outgoing_event_types.rb +9 -0
  56. data/lib/bullet_train/outgoing_webhooks/engine.rb +32 -0
  57. data/lib/bullet_train/outgoing_webhooks/version.rb +1 -1
  58. data/lib/bullet_train/outgoing_webhooks.rb +28 -3
  59. metadata +81 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79efb9fd4d7c770358101460461a724f999cedf19045814334dbe04493588ada
4
- data.tar.gz: 60097da26f31a894bd041497b13920b0878ee8918f8972c1c3a3a78d87ef74b2
3
+ metadata.gz: a4d3884adf8d199b92ccf09ca749e8237e86a421224de5b8b81cf5ab0d9f4329
4
+ data.tar.gz: cec7b468644c484f8bcb0f4064b74764ab3e7b8873cb72ba5c8944009b95a576
5
5
  SHA512:
6
- metadata.gz: 94444641ef863656b88cda03600e52967bcac8c2e864969673d66e98a0d0dd7e562446fad3a192044dee7a9a29eef99590862635e6dae668279964726f9d58ef
7
- data.tar.gz: db6681ef6aec68bffc61c5b476aa6fb2349b849062813cf70e6af0a70b492e7b12fb269d2a77f8ae89f5ba4379ee2c11c3265bcf2cf7649806f4cf080e823077
6
+ metadata.gz: 98d7265e65165a0c2b55a0b2880063f00c72292e0a461e4bde667d54f077860f1b4861873f5131bd203e45b85ab03868822970071e626c34e2ed640059e5cc11
7
+ data.tar.gz: f36d57ccd33ba9268a106ef22c1457560f9b171ff0533b4f8102301e5962691f827ffa437c7f13d3fd7ab847b1b626a9c6374cca45efbbcab228caf7c265d2a9
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2022 Andrew Culver
1
+ Copyright 2022 Bullet Train, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -1,5 +1,6 @@
1
1
  class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationController
2
- account_load_and_authorize_resource :endpoint, through: :team, through_association: :webhooks_outgoing_endpoints
2
+ account_load_and_authorize_resource :endpoint, through: BulletTrain::OutgoingWebhooks.parent_association, through_association: :webhooks_outgoing_endpoints
3
+ before_action { @parent = instance_variable_get("@#{BulletTrain::OutgoingWebhooks.parent_association}") }
3
4
 
4
5
  # GET /account/teams/:team_id/webhooks/outgoing/endpoints
5
6
  # GET /account/teams/:team_id/webhooks/outgoing/endpoints.json
@@ -26,7 +27,7 @@ class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationCon
26
27
  def create
27
28
  respond_to do |format|
28
29
  if @endpoint.save
29
- format.html { redirect_to [:account, @team, :webhooks_outgoing_endpoints], notice: I18n.t("webhooks/outgoing/endpoints.notifications.created") }
30
+ format.html { redirect_to [:account, @parent, :webhooks_outgoing_endpoints], notice: I18n.t("webhooks/outgoing/endpoints.notifications.created") }
30
31
  format.json { render :show, status: :created, location: [:account, @endpoint] }
31
32
  else
32
33
  format.html { render :new, status: :unprocessable_entity }
@@ -54,7 +55,7 @@ class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationCon
54
55
  def destroy
55
56
  @endpoint.destroy
56
57
  respond_to do |format|
57
- format.html { redirect_to [:account, @team, :webhooks_outgoing_endpoints], notice: I18n.t("webhooks/outgoing/endpoints.notifications.destroyed") }
58
+ format.html { redirect_to [:account, @parent, :webhooks_outgoing_endpoints], notice: I18n.t("webhooks/outgoing/endpoints.notifications.destroyed") }
58
59
  format.json { head :no_content }
59
60
  end
60
61
  end
@@ -0,0 +1,63 @@
1
+ class Api::V1::Webhooks::Outgoing::DeliveriesEndpoint < Api::V1::Root
2
+ helpers do
3
+ params :endpoint_id do
4
+ requires :endpoint_id, type: Integer, allow_blank: false, desc: "Endpoint ID"
5
+ end
6
+
7
+ params :id do
8
+ requires :id, type: Integer, allow_blank: false, desc: "Delivery ID"
9
+ end
10
+
11
+ params :delivery do
12
+ optional :event_id, type: String, desc: Api.heading(:event_id)
13
+ optional :endpoint_url, type: String, desc: Api.heading(:endpoint_url)
14
+ optional :delivered_at, type: DateTime, desc: Api.heading(:delivered_at)
15
+ # 🚅 super scaffolding will insert new fields above this line.
16
+ # 🚅 super scaffolding will insert new arrays above this line.
17
+
18
+ # 🚅 super scaffolding will insert processing for new fields above this line.
19
+ end
20
+ end
21
+
22
+ resource "webhooks/outgoing/endpoints", desc: Api.title(:collection_actions) do
23
+ after_validation do
24
+ load_and_authorize_api_resource Webhooks::Outgoing::Delivery
25
+ end
26
+
27
+ #
28
+ # INDEX
29
+ #
30
+
31
+ desc Api.title(:index), &Api.index_desc
32
+ params do
33
+ use :endpoint_id
34
+ end
35
+ oauth2
36
+ paginate per_page: 100
37
+ get "/:endpoint_id/deliveries" do
38
+ @paginated_deliveries = paginate @deliveries
39
+ render @paginated_deliveries, serializer: Api.serializer
40
+ end
41
+ end
42
+
43
+ resource "webhooks/outgoing/deliveries", desc: Api.title(:member_actions) do
44
+ after_validation do
45
+ load_and_authorize_api_resource Webhooks::Outgoing::Delivery
46
+ end
47
+
48
+ #
49
+ # SHOW
50
+ #
51
+
52
+ desc Api.title(:show), &Api.show_desc
53
+ params do
54
+ use :id
55
+ end
56
+ oauth2
57
+ route_param :id do
58
+ get do
59
+ render @delivery, serializer: Api.serializer
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,65 @@
1
+ class Api::V1::Webhooks::Outgoing::DeliveryAttemptsEndpoint < Api::V1::Root
2
+ helpers do
3
+ params :delivery_id do
4
+ requires :delivery_id, type: Integer, allow_blank: false, desc: "Delivery ID"
5
+ end
6
+
7
+ params :id do
8
+ requires :id, type: Integer, allow_blank: false, desc: "Delivery Attempt ID"
9
+ end
10
+
11
+ params :delivery_attempt do
12
+ optional :response_code, type: String, desc: Api.heading(:response_code)
13
+ optional :response_body, type: String, desc: Api.heading(:response_body)
14
+ optional :response_message, type: String, desc: Api.heading(:response_message)
15
+ optional :error_message, type: String, desc: Api.heading(:error_message)
16
+ optional :attempt_number, type: String, desc: Api.heading(:attempt_number)
17
+ # 🚅 super scaffolding will insert new fields above this line.
18
+ # 🚅 super scaffolding will insert new arrays above this line.
19
+
20
+ # 🚅 super scaffolding will insert processing for new fields above this line.
21
+ end
22
+ end
23
+
24
+ resource "webhooks/outgoing/deliveries", desc: Api.title(:collection_actions) do
25
+ after_validation do
26
+ load_and_authorize_api_resource Webhooks::Outgoing::DeliveryAttempt
27
+ end
28
+
29
+ #
30
+ # INDEX
31
+ #
32
+
33
+ desc Api.title(:index), &Api.index_desc
34
+ params do
35
+ use :delivery_id
36
+ end
37
+ oauth2
38
+ paginate per_page: 100
39
+ get "/:delivery_id/delivery_attempts" do
40
+ @paginated_delivery_attempts = paginate @delivery_attempts
41
+ render @paginated_delivery_attempts, serializer: Api.serializer
42
+ end
43
+ end
44
+
45
+ resource "webhooks/outgoing/delivery_attempts", desc: Api.title(:member_actions) do
46
+ after_validation do
47
+ load_and_authorize_api_resource Webhooks::Outgoing::DeliveryAttempt
48
+ end
49
+
50
+ #
51
+ # SHOW
52
+ #
53
+
54
+ desc Api.title(:show), &Api.show_desc
55
+ params do
56
+ use :id
57
+ end
58
+ oauth2
59
+ route_param :id do
60
+ get do
61
+ render @delivery_attempt, serializer: Api.serializer
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,119 @@
1
+ class Api::V1::Webhooks::Outgoing::EndpointsEndpoint < Api::V1::Root
2
+ helpers do
3
+ params BulletTrain::OutgoingWebhooks.parent_association_id do
4
+ requires BulletTrain::OutgoingWebhooks.parent_association_id, type: Integer, allow_blank: false, desc: "#{BulletTrain::OutgoingWebhooks.parent_class} ID"
5
+ end
6
+
7
+ params :id do
8
+ requires :id, type: Integer, allow_blank: false, desc: "Endpoint ID"
9
+ end
10
+
11
+ params :endpoint do
12
+ optional :name, type: String, desc: Api.heading(:name)
13
+ optional :url, type: String, desc: Api.heading(:url)
14
+ # 🚅 super scaffolding will insert new fields above this line.
15
+ optional :event_type_ids, type: Array, desc: Api.heading(:event_type_ids)
16
+ # 🚅 super scaffolding will insert new arrays above this line.
17
+
18
+ # 🚅 super scaffolding will insert processing for new fields above this line.
19
+ end
20
+ end
21
+
22
+ resource BulletTrain::OutgoingWebhooks.parent_resource, desc: Api.title(:collection_actions) do
23
+ after_validation do
24
+ load_and_authorize_api_resource Webhooks::Outgoing::Endpoint
25
+ end
26
+
27
+ #
28
+ # INDEX
29
+ #
30
+
31
+ desc Api.title(:index), &Api.index_desc
32
+ params do
33
+ use BulletTrain::OutgoingWebhooks.parent_association_id
34
+ end
35
+ oauth2
36
+ paginate per_page: 100
37
+ get "/:#{BulletTrain::OutgoingWebhooks.parent_association_id}/webhooks/outgoing/endpoints" do
38
+ @paginated_endpoints = paginate @endpoints
39
+ render @paginated_endpoints, serializer: Api.serializer
40
+ end
41
+
42
+ #
43
+ # CREATE
44
+ #
45
+
46
+ desc Api.title(:create), &Api.create_desc
47
+ params do
48
+ use BulletTrain::OutgoingWebhooks.parent_association_id
49
+ use :endpoint
50
+ end
51
+ route_setting :api_resource_options, permission: :create
52
+ oauth2 "write"
53
+ post "/:#{BulletTrain::OutgoingWebhooks.parent_association_id}/webhooks/outgoing/endpoints" do
54
+ if @endpoint.save
55
+ render @endpoint, serializer: Api.serializer
56
+ else
57
+ record_not_saved @endpoint
58
+ end
59
+ end
60
+ end
61
+
62
+ resource "webhooks/outgoing/endpoints", desc: Api.title(:member_actions) do
63
+ after_validation do
64
+ load_and_authorize_api_resource Webhooks::Outgoing::Endpoint
65
+ end
66
+
67
+ #
68
+ # SHOW
69
+ #
70
+
71
+ desc Api.title(:show), &Api.show_desc
72
+ params do
73
+ use :id
74
+ end
75
+ oauth2
76
+ route_param :id do
77
+ get do
78
+ render @endpoint, serializer: Api.serializer
79
+ end
80
+ end
81
+
82
+ #
83
+ # UPDATE
84
+ #
85
+
86
+ desc Api.title(:update), &Api.update_desc
87
+ params do
88
+ use :id
89
+ use :endpoint
90
+ end
91
+ route_setting :api_resource_options, permission: :update
92
+ oauth2 "write"
93
+ route_param :id do
94
+ put do
95
+ if @endpoint.update(declared(params, include_missing: false))
96
+ render @endpoint, serializer: Api.serializer
97
+ else
98
+ record_not_saved @endpoint
99
+ end
100
+ end
101
+ end
102
+
103
+ #
104
+ # DESTROY
105
+ #
106
+
107
+ desc Api.title(:destroy), &Api.destroy_desc
108
+ params do
109
+ use :id
110
+ end
111
+ route_setting :api_resource_options, permission: :destroy
112
+ oauth2 "delete"
113
+ route_param :id do
114
+ delete do
115
+ render @endpoint.destroy, serializer: Api.serializer
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,7 @@
1
+ class Webhooks::Outgoing::DeliveryJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(delivery)
5
+ delivery.deliver
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class Webhooks::Outgoing::GenerateJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform(obj, action)
5
+ obj.generate_webhook_perform(action)
6
+ end
7
+ end
@@ -0,0 +1,64 @@
1
+ module Webhooks::Outgoing::DeliveryAttemptSupport
2
+ extend ActiveSupport::Concern
3
+ include Webhooks::Outgoing::UriFiltering
4
+
5
+ SUCCESS_RESPONSE_CODES = [200, 201, 202, 203, 204, 205, 206, 207, 226].freeze
6
+
7
+ included do
8
+ belongs_to :delivery
9
+ has_one :team, through: :delivery unless BulletTrain::OutgoingWebhooks.parent_class_specified?
10
+ scope :successful, -> { where(response_code: SUCCESS_RESPONSE_CODES) }
11
+
12
+ before_create do
13
+ self.attempt_number = delivery.attempt_count + 1
14
+ end
15
+
16
+ validates :response_code, presence: true
17
+ end
18
+
19
+ def still_attempting?
20
+ error_message.nil? && response_code.nil?
21
+ end
22
+
23
+ def successful?
24
+ SUCCESS_RESPONSE_CODES.include?(response_code)
25
+ end
26
+
27
+ def failed?
28
+ !(successful? || still_attempting?)
29
+ end
30
+
31
+ def attempt
32
+ uri = URI.parse(delivery.endpoint_url)
33
+
34
+ unless allowed_uri?(uri)
35
+ self.response_code = 0
36
+ self.error_message = "URI is not allowed: " + uri
37
+ return false
38
+ end
39
+
40
+ http = Net::HTTP.new(resolve_ip_from_authoritative(uri.hostname.downcase), uri.port)
41
+ http.use_ssl = true if uri.scheme == "https"
42
+ request = Net::HTTP::Post.new(uri.path)
43
+ request.add_field("Host", uri.host)
44
+ request.add_field("Content-Type", "application/json")
45
+ request.body = delivery.event.payload.to_json
46
+
47
+ begin
48
+ response = http.request(request)
49
+ self.response_message = response.message
50
+ self.response_code = response.code
51
+ self.response_body = response.body
52
+ rescue => exception
53
+ self.response_code = 0
54
+ self.error_message = exception.message
55
+ end
56
+
57
+ save
58
+ successful?
59
+ end
60
+
61
+ def label_string
62
+ "#{attempt_number.ordinalize} Attempt"
63
+ end
64
+ end
@@ -0,0 +1,66 @@
1
+ module Webhooks::Outgoing::DeliverySupport
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ belongs_to :endpoint, class_name: "Webhooks::Outgoing::Endpoint"
6
+ belongs_to :event, class_name: "Webhooks::Outgoing::Event"
7
+
8
+ has_one :team, through: :endpoint unless BulletTrain::OutgoingWebhooks.parent_class_specified?
9
+ has_many :delivery_attempts, class_name: "Webhooks::Outgoing::DeliveryAttempt", dependent: :destroy, foreign_key: :delivery_id
10
+ end
11
+
12
+ ATTEMPT_SCHEDULE = {
13
+ 1 => 15.seconds,
14
+ 2 => 1.minute,
15
+ 3 => 5.minutes,
16
+ 4 => 15.minutes,
17
+ 5 => 1.hour,
18
+ }
19
+
20
+ def label_string
21
+ event.short_uuid
22
+ end
23
+
24
+ def next_reattempt_delay
25
+ ATTEMPT_SCHEDULE[attempt_count]
26
+ end
27
+
28
+ def deliver_async
29
+ if still_attempting?
30
+ Webhooks::Outgoing::DeliveryJob.set(wait: next_reattempt_delay).perform_later(self)
31
+ end
32
+ end
33
+
34
+ def deliver
35
+ if delivery_attempts.create.attempt
36
+ touch(:delivered_at)
37
+ else
38
+ deliver_async
39
+ end
40
+ end
41
+
42
+ def attempt_count
43
+ delivery_attempts.count
44
+ end
45
+
46
+ def delivered?
47
+ delivered_at.present?
48
+ end
49
+
50
+ def still_attempting?
51
+ return false if delivered?
52
+ attempt_count < max_attempts
53
+ end
54
+
55
+ def failed?
56
+ !(delivered? || still_attempting?)
57
+ end
58
+
59
+ def name
60
+ event.short_uuid
61
+ end
62
+
63
+ def max_attempts
64
+ ATTEMPT_SCHEDULE.keys.max
65
+ end
66
+ end
@@ -0,0 +1,37 @@
1
+ module Webhooks::Outgoing::EndpointSupport
2
+ extend ActiveSupport::Concern
3
+ include Webhooks::Outgoing::UriFiltering
4
+
5
+ included do
6
+ belongs_to BulletTrain::OutgoingWebhooks.parent_association
7
+
8
+ has_many :deliveries, class_name: "Webhooks::Outgoing::Delivery", dependent: :destroy, foreign_key: :endpoint_id
9
+ has_many :events, -> { distinct }, through: :deliveries
10
+
11
+ scope :listening_for_event_type_id, ->(event_type_id) { where("event_type_ids @> ? OR event_type_ids = '[]'::jsonb", "\"#{event_type_id}\"") }
12
+
13
+ validates :name, presence: true
14
+
15
+ before_validation { url&.strip! }
16
+
17
+ validates :url, presence: true, allowed_uri: true
18
+
19
+ after_initialize do
20
+ self.event_type_ids ||= []
21
+ end
22
+
23
+ after_save :touch_parent
24
+ end
25
+
26
+ def valid_event_types
27
+ Webhooks::Outgoing::EventType.all
28
+ end
29
+
30
+ def event_types
31
+ event_type_ids.map { |id| Webhooks::Outgoing::EventType.find(id) }
32
+ end
33
+
34
+ def touch_parent
35
+ send(BulletTrain::OutgoingWebhooks.parent_association).touch
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ module Webhooks::Outgoing::EventSupport
2
+ extend ActiveSupport::Concern
3
+ include HasUuid
4
+
5
+ included do
6
+ belongs_to BulletTrain::OutgoingWebhooks.parent_association
7
+ belongs_to :event_type, class_name: "Webhooks::Outgoing::EventType"
8
+ belongs_to :subject, polymorphic: true
9
+ has_many :deliveries, dependent: :destroy
10
+
11
+ before_create do
12
+ self.payload = generate_payload
13
+ end
14
+ end
15
+
16
+ def generate_payload
17
+ {
18
+ event_id: uuid,
19
+ event_type: event_type_id,
20
+ subject_id: subject_id,
21
+ subject_type: subject_type,
22
+ data: data
23
+ }
24
+ end
25
+
26
+ def event_type_name
27
+ payload.dig("event_type")
28
+ end
29
+
30
+ def endpoints
31
+ send(BulletTrain::OutgoingWebhooks.parent_association).webhooks_outgoing_endpoints.listening_for_event_type_id(event_type_id)
32
+ end
33
+
34
+ def deliver
35
+ endpoints.each do |endpoint|
36
+ unless endpoint.deliveries.where(event: self).any?
37
+ endpoint.deliveries.create(event: self, endpoint_url: endpoint.url).deliver_async
38
+ end
39
+ end
40
+ end
41
+
42
+ def label_string
43
+ short_uuid
44
+ end
45
+ end
@@ -0,0 +1,67 @@
1
+ module Webhooks::Outgoing::IssuingModel
2
+ extend ActiveSupport::Concern
3
+
4
+ # define relationships.
5
+ included do
6
+ after_commit :generate_created_webhook, on: [:create]
7
+ after_commit :generate_updated_webhook, on: [:update]
8
+ after_commit :generate_deleted_webhook, on: [:destroy]
9
+ has_many :webhooks_outgoing_events, as: :subject, class_name: "Webhooks::Outgoing::Event", dependent: :nullify
10
+ end
11
+
12
+ def skip_generate_webhook?(action)
13
+ false
14
+ end
15
+
16
+ def parent
17
+ return unless respond_to? BulletTrain::OutgoingWebhooks.parent_association
18
+ send(BulletTrain::OutgoingWebhooks.parent_association)
19
+ end
20
+
21
+ def generate_webhook(action, async: true)
22
+ # allow individual models to opt out of generating webhooks
23
+ return if skip_generate_webhook?(action)
24
+
25
+ # we can only generate webhooks for objects that return their their team / parent.
26
+ return unless parent.present?
27
+
28
+ # Try to find an event type definition for this action.
29
+ event_type = Webhooks::Outgoing::EventType.find_by(id: "#{self.class.name.underscore}.#{action}")
30
+
31
+ # If the event type is defined as one that people can be subscribed to,
32
+ # and this object has a parent where an associated outgoing webhooks endpoint could be registered.
33
+ if event_type
34
+ # Only generate an event record if an endpoint is actually listening for this event type.
35
+ if parent.endpoints_listening_for_event_type?(event_type)
36
+ if async
37
+ # serialization can be heavy so run it as a job
38
+ Webhooks::Outgoing::GenerateJob.perform_later(self, action)
39
+ else
40
+ generate_webhook_perform(action)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def generate_webhook_perform(action)
47
+ event_type = Webhooks::Outgoing::EventType.find_by(id: "#{self.class.name.underscore}.#{action}")
48
+ data = "Api::V1::#{self.class.name}Serializer".constantize.new(self).serializable_hash[:data]
49
+ webhook = send(BulletTrain::OutgoingWebhooks.parent_association).webhooks_outgoing_events.create(event_type_id: event_type.id, subject: self, data: data)
50
+ webhook.deliver
51
+ end
52
+
53
+ def generate_created_webhook
54
+ generate_webhook(:created)
55
+ end
56
+
57
+ def generate_updated_webhook
58
+ generate_webhook(:updated)
59
+ end
60
+
61
+ def generate_deleted_webhook
62
+ return false unless parent.present?
63
+ return false if parent.being_destroyed?
64
+
65
+ generate_webhook(:deleted, async: false)
66
+ end
67
+ end
@@ -0,0 +1,31 @@
1
+ module Webhooks::Outgoing::TeamSupport
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :webhooks_outgoing_endpoints, class_name: "Webhooks::Outgoing::Endpoint", dependent: :destroy
6
+ has_many :webhooks_outgoing_events, class_name: "Webhooks::Outgoing::Event", dependent: :destroy
7
+
8
+ before_destroy :mark_for_destruction, prepend: true
9
+ end
10
+
11
+ def should_cache_endpoints_listening_for_event_type?
12
+ true
13
+ end
14
+
15
+ def endpoints_listening_for_event_type?(event_type)
16
+ if should_cache_endpoints_listening_for_event_type?
17
+ key = "#{cache_key_with_version}/endpoints_for_event_type/#{event_type.cache_key}"
18
+
19
+ Rails.cache.fetch(key, expires_in: 24.hours, race_condition_ttl: 5.seconds) do
20
+ webhooks_outgoing_endpoints.listening_for_event_type_id(event_type.id).any?
21
+ end
22
+ else
23
+ webhooks_outgoing_endpoints.listening_for_event_type_id(event_type.id).any?
24
+ end
25
+ end
26
+
27
+ def mark_for_destruction
28
+ # This allows downstream logic to check whether a team is being destroyed in order to bypass webhook issuance.
29
+ update_column(:being_destroyed, true)
30
+ end
31
+ end