webhooks-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +20 -0
  5. data/app/assets/config/webhooks_manifest.js +1 -0
  6. data/app/assets/stylesheets/webhooks/application.css +15 -0
  7. data/app/controllers/webhooks/application_controller.rb +6 -0
  8. data/app/controllers/webhooks/attempts_controller.rb +25 -0
  9. data/app/controllers/webhooks/endpoints_controller.rb +65 -0
  10. data/app/controllers/webhooks/events_controller.rb +37 -0
  11. data/app/helpers/webhooks/application_helper.rb +6 -0
  12. data/app/jobs/webhooks/application_job.rb +6 -0
  13. data/app/jobs/webhooks/deliver_job.rb +11 -0
  14. data/app/mailers/webhooks/application_mailer.rb +8 -0
  15. data/app/models/webhooks/application_record.rb +7 -0
  16. data/app/models/webhooks/attempt.rb +71 -0
  17. data/app/models/webhooks/endpoint.rb +74 -0
  18. data/app/models/webhooks/event.rb +50 -0
  19. data/app/models/webhooks/event_serializer.rb +30 -0
  20. data/app/models/webhooks/request.rb +37 -0
  21. data/app/models/webhooks/response.rb +29 -0
  22. data/app/views/layouts/webhooks/application.html.erb +29 -0
  23. data/app/views/webhooks/attempts/_attempt.html.erb +32 -0
  24. data/app/views/webhooks/attempts/show.html.erb +33 -0
  25. data/app/views/webhooks/endpoints/_endpoint.html.erb +31 -0
  26. data/app/views/webhooks/endpoints/_form.html.erb +42 -0
  27. data/app/views/webhooks/endpoints/edit.html.erb +16 -0
  28. data/app/views/webhooks/endpoints/index.html.erb +41 -0
  29. data/app/views/webhooks/endpoints/new.html.erb +16 -0
  30. data/app/views/webhooks/endpoints/show.html.erb +102 -0
  31. data/app/views/webhooks/events/_event.html.erb +11 -0
  32. data/app/views/webhooks/events/index.html.erb +38 -0
  33. data/app/views/webhooks/events/new.html.erb +35 -0
  34. data/app/views/webhooks/events/show.html.erb +49 -0
  35. data/app/views/webhooks/requests/_request.html.erb +47 -0
  36. data/app/views/webhooks/responses/_response.html.erb +55 -0
  37. data/app/views/webhooks/shared/_navigation.html.erb +100 -0
  38. data/config/routes.rb +13 -0
  39. data/db/migrate/20210518022959_create_webhooks_endpoints.rb +17 -0
  40. data/db/migrate/20210518043350_create_webhooks_events.rb +12 -0
  41. data/db/migrate/20210518050123_create_webhooks_attempts.rb +36 -0
  42. data/db/migrate/20210518054916_create_webhooks_requests.rb +14 -0
  43. data/db/migrate/20210518060614_create_webhooks_responses.rb +15 -0
  44. data/lib/tasks/webhooks_tasks.rake +5 -0
  45. data/lib/webhooks-rails.rb +3 -0
  46. data/lib/webhooks.rb +21 -0
  47. data/lib/webhooks/engine.rb +31 -0
  48. data/lib/webhooks/version.rb +5 -0
  49. metadata +183 -0
@@ -0,0 +1,11 @@
1
+ <tr>
2
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
3
+ <%= event %>
4
+ </td>
5
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
6
+ <%= event.event %>
7
+ </td>
8
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">
9
+ <%= link_to event.created_at, event %>
10
+ </td>
11
+ </tr>
@@ -0,0 +1,38 @@
1
+ <div class="py-6">
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 md:flex md:justify-between md:space-x-5">
3
+ <h1 class="text-2xl font-semibold text-gray-900">Events</h1>
4
+ <div class="mt-6 flex flex-col-reverse justify-stretch space-y-4 space-y-reverse sm:flex-row-reverse sm:justify-end sm:space-x-reverse sm:space-y-0 sm:space-x-3 md:mt-0 md:flex-row md:space-x-3">
5
+ <%= link_to 'New Event', new_event_path, class: 'inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-blue-500' %>
6
+ </div>
7
+ </div>
8
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
9
+ <div class="py-4">
10
+ <div class="flex flex-col">
11
+ <div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
12
+ <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
13
+ <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
14
+ <table class="min-w-full divide-y divide-gray-200">
15
+ <thead class="bg-gray-50">
16
+ <tr>
17
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
18
+ Event Type
19
+ </th>
20
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
21
+ Event
22
+ </th>
23
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
24
+ Timestamp
25
+ </th>
26
+ </tr>
27
+ </thead>
28
+ <tbody class="bg-white divide-y divide-gray-200">
29
+ <%= render @events %>
30
+ </tbody>
31
+ </table>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
@@ -0,0 +1,35 @@
1
+ <div class="py-6">
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
3
+ <h1 class="text-2xl font-semibold text-gray-900">New Event</h1>
4
+ </div>
5
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
6
+ <div class="py-4">
7
+ <div class="flex flex-col">
8
+ <div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
9
+ <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
10
+ <%= form_with model: @event do |form| %>
11
+ <div class="space-y-6">
12
+ <div>
13
+ <%= form.label :event_type, class: 'block text-sm font-medium text-gray-700' %>
14
+ <%= form.collection_select :event_type, Rails.application.config.webhooks.events.types, :to_s, :to_s, prompt: true, class: "select" %>
15
+ </div>
16
+
17
+ <div>
18
+ <%= form.label :event, class: 'block text-sm font-medium text-gray-700' %>
19
+ <div class="mt-1">
20
+ <%= form.text_area :event, value: '{"action":"user.created"}', rows: 25, class: 'block w-full shadow-sm focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm border-gray-300 rounded-md py-2 px-4 font-mono', placeholder: 'https://example.com/webhook' %>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="flex justify-end">
25
+ <%= link_to 'Cancel', events_path, class: 'bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500' %>
26
+ <%= form.submit 'Create Event', class: 'cursor-pointer ml-3 inline-flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500' %>
27
+ </div>
28
+ </div>
29
+ <% end %>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
@@ -0,0 +1,49 @@
1
+ <div class="py-6">
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 md:flex md:justify-between md:space-x-5">
3
+ <h1 class="text-2xl font-semibold text-gray-900"><%= @event %></h1>
4
+ </div>
5
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
6
+ <div class="py-4">
7
+ <div class="flex flex-col">
8
+ <div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
9
+ <div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8 space-y-6">
10
+ <pre><%= @event.event %></pre>
11
+
12
+ <div>
13
+ <h3 class="text-lg leading-6 font-medium text-gray-900">
14
+ Attempts
15
+ </h3>
16
+ <p class="mt-2 max-w-4xl text-sm text-gray-500">
17
+ All webhook delivery attempts and their status.
18
+ </p>
19
+ </div>
20
+
21
+ <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
22
+ <table class="min-w-full divide-y divide-gray-200">
23
+ <thead class="bg-gray-50">
24
+ <tr>
25
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
26
+ Status
27
+ </th>
28
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
29
+ Event Type
30
+ </th>
31
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
32
+ Endpoint
33
+ </th>
34
+ <th scope="col" class="relative px-6 py-3">
35
+ <span class="sr-only">Edit</span>
36
+ </th>
37
+ </tr>
38
+ </thead>
39
+ <tbody class="bg-white divide-y divide-gray-200">
40
+ <%= render @event.attempts.order(created_at: :desc) %>
41
+ </tbody>
42
+ </table>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
@@ -0,0 +1,47 @@
1
+ <div class="space-y-6">
2
+ <div>
3
+ <h3 class="text-lg leading-6 font-medium text-gray-900">
4
+ Request
5
+ </h3>
6
+ <p class="mt-2 max-w-4xl text-sm text-gray-500">
7
+ The request delivered to <%= @attempt.endpoint %>
8
+ </p>
9
+ </div>
10
+
11
+ <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
12
+ <table class="min-w-full divide-y divide-gray-200">
13
+ <thead class="bg-gray-50">
14
+ <tr>
15
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
16
+ Header
17
+ </th>
18
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
19
+ Value
20
+ </th>
21
+ </tr>
22
+ </thead>
23
+ <tbody class="bg-white divide-y divide-gray-200">
24
+ <% request.headers.each do |key, value| %>
25
+ <tr>
26
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
27
+ <%= key %>
28
+ </td>
29
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
30
+ <%= value %>
31
+ </td>
32
+ </tr>
33
+ <% end %>
34
+ </tbody>
35
+ </table>
36
+ </div>
37
+
38
+ <div>
39
+ <h3 class="text-lg leading-6 font-medium text-gray-900">
40
+ Request Body
41
+ </h3>
42
+ <p class="mt-2 max-w-4xl text-sm text-gray-500">
43
+ The request delivered to <%= @attempt.endpoint %>
44
+ </p>
45
+ </div>
46
+ <div class="bg-white shadow overflow-hidden border-b border-gray-200 sm:rounded-lg px-6 py-3"><pre><%= request.body %></pre></div>
47
+ </div>
@@ -0,0 +1,55 @@
1
+ <div class="space-y-6">
2
+ <div>
3
+ <h3 class="text-lg leading-6 font-medium text-gray-900">
4
+ Response
5
+ </h3>
6
+ <p class="mt-2 max-w-4xl text-sm text-gray-500">
7
+ The response headers received from <%= response.endpoint %>
8
+ </p>
9
+ </div>
10
+
11
+ <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
12
+ <table class="min-w-full divide-y divide-gray-200">
13
+ <thead class="bg-gray-50">
14
+ <tr>
15
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
16
+ Header
17
+ </th>
18
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
19
+ Value
20
+ </th>
21
+ </tr>
22
+ </thead>
23
+ <tbody class="bg-white divide-y divide-gray-200">
24
+ <tr>
25
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
26
+ Status
27
+ </td>
28
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
29
+ <%= response.status_code %>
30
+ </td>
31
+ </tr>
32
+ <% response.headers.each do |key, value| %>
33
+ <tr>
34
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
35
+ <%= key %>
36
+ </td>
37
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
38
+ <%= value %>
39
+ </td>
40
+ </tr>
41
+ <% end %>
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+
46
+ <div>
47
+ <h3 class="text-lg leading-6 font-medium text-gray-900">
48
+ Response Body
49
+ </h3>
50
+ <p class="mt-2 max-w-4xl text-sm text-gray-500">
51
+ The response body received from <%= response.endpoint %>
52
+ </p>
53
+ </div>
54
+ <div class="bg-white shadow overflow-hidden border-b border-gray-200 sm:rounded-lg px-6 py-3"><pre><%= response.body %></pre></div>
55
+ </div>
@@ -0,0 +1,100 @@
1
+ <!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. -->
2
+ <div class="fixed inset-0 flex z-40 md:hidden" role="dialog" aria-modal="true">
3
+ <!--
4
+ Off-canvas menu overlay, show/hide based on off-canvas menu state.
5
+
6
+ Entering: "transition-opacity ease-linear duration-300"
7
+ From: "opacity-0"
8
+ To: "opacity-100"
9
+ Leaving: "transition-opacity ease-linear duration-300"
10
+ From: "opacity-100"
11
+ To: "opacity-0"
12
+ -->
13
+ <div class="fixed inset-0 bg-gray-600 bg-opacity-75" aria-hidden="true"></div>
14
+
15
+ <!--
16
+ Off-canvas menu, show/hide based on off-canvas menu state.
17
+
18
+ Entering: "transition ease-in-out duration-300 transform"
19
+ From: "-translate-x-full"
20
+ To: "translate-x-0"
21
+ Leaving: "transition ease-in-out duration-300 transform"
22
+ From: "translate-x-0"
23
+ To: "-translate-x-full"
24
+ -->
25
+ <div class="relative flex-1 flex flex-col max-w-xs w-full bg-white">
26
+ <!--
27
+ Close button, show/hide based on off-canvas menu state.
28
+
29
+ Entering: "ease-in-out duration-300"
30
+ From: "opacity-0"
31
+ To: "opacity-100"
32
+ Leaving: "ease-in-out duration-300"
33
+ From: "opacity-100"
34
+ To: "opacity-0"
35
+ -->
36
+ <div class="absolute top-0 right-0 -mr-12 pt-2">
37
+ <button class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
38
+ <span class="sr-only">Close sidebar</span>
39
+ <!-- Heroicon name: outline/x -->
40
+ <svg class="h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
41
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
42
+ </svg>
43
+ </button>
44
+ </div>
45
+
46
+ <div class="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
47
+ <div class="flex-shrink-0 flex items-center px-4">
48
+ Webhooks
49
+ </div>
50
+ <nav class="mt-5 px-2 space-y-1">
51
+ <%= link_to endpoints_path, class: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 group flex items-center px-2 py-2 text-base font-medium rounded-md' do %>
52
+ <svg class="text-gray-500 mr-4 flex-shrink-0 h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
53
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
54
+ </svg>
55
+ Endpoints
56
+ <% end %>
57
+
58
+ <%= link_to events_path, class: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 group flex items-center px-2 py-2 text-base font-medium rounded-md' do %>
59
+ <svg class="text-gray-500 mr-4 flex-shrink-0 h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
60
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
61
+ </svg>
62
+ Events
63
+ <% end %>
64
+ </nav>
65
+ </div>
66
+ </div>
67
+
68
+ <div class="flex-shrink-0 w-14">
69
+ <!-- Force sidebar to shrink to fit close icon -->
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Static sidebar for desktop -->
74
+ <div class="hidden md:flex md:flex-shrink-0">
75
+ <div class="flex flex-col w-64">
76
+ <!-- Sidebar component, swap this element with another sidebar if you like -->
77
+ <div class="flex flex-col h-0 flex-1 border-r border-gray-200 bg-white">
78
+ <div class="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
79
+ <div class="flex items-center flex-shrink-0 px-4">
80
+ Webhooks
81
+ </div>
82
+ <nav class="mt-5 flex-1 px-2 bg-white space-y-1">
83
+ <%= link_to endpoints_path, class: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 group flex items-center px-2 py-2 text-sm font-medium rounded-md' do %>
84
+ <svg class="text-gray-500 mr-3 flex-shrink-0 h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
85
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
86
+ </svg>
87
+ Endpoints
88
+ <% end %>
89
+
90
+ <%= link_to events_path, class: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900 group flex items-center px-2 py-2 text-sm font-medium rounded-md' do %>
91
+ <svg class="text-gray-500 mr-3 flex-shrink-0 h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
92
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
93
+ </svg>
94
+ Events
95
+ <% end %>
96
+ </nav>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Webhooks::Engine.routes.draw do
4
+ resources :endpoints
5
+ resources :events, only: %i[index new show create]
6
+ resources :attempts, only: :show do
7
+ member do
8
+ post :reattempt
9
+ end
10
+ end
11
+
12
+ get '/' => redirect('/endpoints')
13
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateWebhooksEndpoints < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :webhooks_endpoints do |t|
6
+ t.string :url, null: false, limit: 256
7
+ t.string :event_types, array: true, null: true
8
+ t.integer :state, null: false, default: 0
9
+ t.string :secret, null: false
10
+
11
+ t.timestamps null: false, precision: 6
12
+
13
+ t.index :url, unique: true
14
+ t.index :secret, unique: true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateWebhooksEvents < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :webhooks_events do |t|
6
+ t.string :event_type, null: false
7
+ t.json :event, null: false, default: {}
8
+
9
+ t.timestamps null: false, precision: 6
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateWebhooksAttempts < ActiveRecord::Migration[6.1]
4
+ def change
5
+ reversible do |direction|
6
+ direction.up do
7
+ execute <<-SQL.squish
8
+ DROP INDEX IF EXISTS index_webhooks_attempts_on_endpoint_id_and_event_id_and_attempt;
9
+ SQL
10
+ end
11
+ end
12
+
13
+ create_table :webhooks_attempts do |t|
14
+ t.belongs_to :webhooks_endpoint, null: false, foreign_key: true
15
+ t.belongs_to :webhooks_event, null: false, foreign_key: true
16
+
17
+ # Was this attempt triggered manually?
18
+ t.boolean :manual, null: false, default: false
19
+ t.integer :state, null: false, default: 0
20
+ t.integer :attempt, null: false, default: 1
21
+
22
+ t.datetime :retry_at, null: true, precision: 6
23
+ t.timestamps null: false, precision: 6
24
+
25
+ t.index %i[webhooks_endpoint_id webhooks_event_id attempt], unique: true, name: 'index_webhooks_attempts_on_endpoint_id_and_event_id_and_attempt'
26
+ end
27
+
28
+ reversible do |direction|
29
+ direction.up do
30
+ execute <<-SQL.squish
31
+ CREATE UNIQUE INDEX index_webhooks_attempts_on_endpoint_id_and_event_id_and_attempt ON webhooks_attempts ("webhooks_endpoint_id", "webhooks_event_id", "attempt") WHERE "manual" = 'f';
32
+ SQL
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateWebhooksRequests < ActiveRecord::Migration[6.1]
4
+ def change
5
+ create_table :webhooks_requests do |t|
6
+ t.belongs_to :webhooks_attempt, null: false, foreign_key: true, index: { unique: true }
7
+
8
+ t.json :headers, null: false
9
+ t.json :body, null: false
10
+
11
+ t.datetime :created_at, null: false, precision: 6
12
+ end
13
+ end
14
+ end