contentful-scheduler-custom-build-john 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/Rakefile +7 -0
- data/contentful-scheduler.gemspec +35 -0
- data/example/Gemfile +7 -0
- data/example/Procfile +4 -0
- data/example/Rakefile +46 -0
- data/example/config.ru +13 -0
- data/lib/contentful/scheduler.rb +57 -0
- data/lib/contentful/scheduler/controller.rb +30 -0
- data/lib/contentful/scheduler/queue.rb +102 -0
- data/lib/contentful/scheduler/tasks.rb +1 -0
- data/lib/contentful/scheduler/tasks/publish.rb +21 -0
- data/lib/contentful/scheduler/version.rb +5 -0
- data/spec/contentful/scheduler/controller_spec.rb +46 -0
- data/spec/contentful/scheduler/queue_spec.rb +257 -0
- data/spec/contentful/scheduler/tasks/publish_spec.rb +42 -0
- data/spec/contentful/scheduler_spec.rb +141 -0
- data/spec/spec_helper.rb +60 -0
- metadata +243 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'contentful/webhook/listener'
|
2
|
+
require_relative 'queue'
|
3
|
+
|
4
|
+
module Contentful
|
5
|
+
module Scheduler
|
6
|
+
class Controller < ::Contentful::Webhook::Listener::Controllers::WebhookAware
|
7
|
+
def create
|
8
|
+
return unless webhook.entry?
|
9
|
+
|
10
|
+
logger.info "Queueing - Space: #{webhook.space_id} - Entry: #{webhook.id}"
|
11
|
+
|
12
|
+
Queue.instance(logger).update_or_create(webhook)
|
13
|
+
end
|
14
|
+
alias_method :save, :create
|
15
|
+
alias_method :auto_save, :create
|
16
|
+
alias_method :unarchive, :create
|
17
|
+
|
18
|
+
def delete
|
19
|
+
return unless webhook.entry?
|
20
|
+
|
21
|
+
logger.info "Unqueueing - Space: #{webhook.space_id} - Entry: #{webhook.id}"
|
22
|
+
|
23
|
+
Queue.instance(logger).remove(webhook)
|
24
|
+
end
|
25
|
+
alias_method :unpublish, :delete
|
26
|
+
alias_method :archive, :delete
|
27
|
+
alias_method :publish, :delete
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require_relative "tasks"
|
2
|
+
require 'chronic'
|
3
|
+
require 'contentful/webhook/listener'
|
4
|
+
|
5
|
+
module Contentful
|
6
|
+
module Scheduler
|
7
|
+
class Queue
|
8
|
+
@@instance = nil
|
9
|
+
|
10
|
+
attr_reader :config, :logger
|
11
|
+
|
12
|
+
def self.instance(logger = ::Contentful::Webhook::Listener::Support::NullLogger.new)
|
13
|
+
@@instance ||= new(logger)
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_or_create(webhook)
|
17
|
+
return unless publishable?(webhook)
|
18
|
+
remove(webhook) if in_queue?(webhook)
|
19
|
+
return if already_published?(webhook)
|
20
|
+
|
21
|
+
success = Resque.enqueue_at(
|
22
|
+
publish_date(webhook),
|
23
|
+
::Contentful::Scheduler::Tasks::Publish,
|
24
|
+
webhook.space_id,
|
25
|
+
webhook.id,
|
26
|
+
::Contentful::Scheduler.config[:spaces][webhook.space_id][:management_token]
|
27
|
+
)
|
28
|
+
|
29
|
+
if success
|
30
|
+
logger.info "Webhook {id: #{webhook.id}, space_id: #{webhook.space_id}} successfully added to queue"
|
31
|
+
else
|
32
|
+
logger.warn "Webhook {id: #{webhook.id}, space_id: #{webhook.space_id}} couldn't be added to queue"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove(webhook)
|
37
|
+
return unless publishable?(webhook)
|
38
|
+
return unless in_queue?(webhook)
|
39
|
+
|
40
|
+
success = Resque.remove_delayed(
|
41
|
+
::Contentful::Scheduler::Tasks::Publish,
|
42
|
+
webhook.space_id,
|
43
|
+
webhook.id,
|
44
|
+
::Contentful::Scheduler.config[:management_token]
|
45
|
+
)
|
46
|
+
|
47
|
+
if success
|
48
|
+
logger.info "Webhook {id: #{webhook.id}, space_id: #{webhook.space_id}} successfully removed from queue"
|
49
|
+
else
|
50
|
+
logger.warn "Webhook {id: #{webhook.id}, space_id: #{webhook.space_id}} couldn't be removed from queue"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def publishable?(webhook)
|
55
|
+
return false unless spaces.key?(webhook.space_id)
|
56
|
+
|
57
|
+
if webhook_publish_field?(webhook)
|
58
|
+
return !webhook_publish_field(webhook).nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
def already_published?(webhook)
|
65
|
+
return true if publish_date(webhook) < Time.now.utc
|
66
|
+
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
def in_queue?(webhook)
|
71
|
+
Resque.peek(::Contentful::Scheduler::Tasks::Publish, 0, -1).any? do |job|
|
72
|
+
job['args'][0] == webhook.space_id && job['args'][1] == webhook.id
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def publish_date(webhook)
|
77
|
+
date_field = webhook_publish_field(webhook)
|
78
|
+
date_field = date_field[date_field.keys[0]] if date_field.is_a? Hash
|
79
|
+
Chronic.parse(date_field).utc
|
80
|
+
end
|
81
|
+
|
82
|
+
def spaces
|
83
|
+
config[:spaces]
|
84
|
+
end
|
85
|
+
|
86
|
+
def webhook_publish_field?(webhook)
|
87
|
+
webhook.fields.key?(spaces.fetch(webhook.space_id, {})[:publish_field])
|
88
|
+
end
|
89
|
+
|
90
|
+
def webhook_publish_field(webhook)
|
91
|
+
webhook.fields[spaces[webhook.space_id][:publish_field]]
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def initialize(logger)
|
97
|
+
@config = ::Contentful::Scheduler.config
|
98
|
+
@logger = logger
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'tasks/publish'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'contentful/management'
|
2
|
+
|
3
|
+
module Contentful
|
4
|
+
module Scheduler
|
5
|
+
module Tasks
|
6
|
+
class Publish
|
7
|
+
@queue = :publish
|
8
|
+
|
9
|
+
def self.perform(space_id, entry_id, token)
|
10
|
+
client = ::Contentful::Management::Client.new(
|
11
|
+
token,
|
12
|
+
raise_errors: true,
|
13
|
+
application_name: 'contentful-scheduler',
|
14
|
+
application_version: Contentful::Scheduler::VERSION
|
15
|
+
)
|
16
|
+
client.entries.find(space_id, entry_id).publish
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Contentful::Scheduler::Controller do
|
4
|
+
let(:server) { MockServer.new }
|
5
|
+
let(:logger) { Contentful::Webhook::Listener::Support::NullLogger.new }
|
6
|
+
let(:timeout) { 10 }
|
7
|
+
let(:headers) { {'X-Contentful-Topic' => '', 'X-Contentful-Webhook-Name' => 'SomeName'} }
|
8
|
+
let(:body) { {sys: { id: 'foo', space: { sys: { id: 'space_foo' } } }, fields: {} } }
|
9
|
+
let(:queue) { ::Contentful::Scheduler::Queue.instance }
|
10
|
+
subject { described_class.new server, logger, timeout }
|
11
|
+
|
12
|
+
describe 'events' do
|
13
|
+
[:create, :save, :auto_save, :unarchive].each do |event|
|
14
|
+
it "creates or updates webhook metadata in publish queue on #{event}" do
|
15
|
+
expect(queue).to receive(:update_or_create)
|
16
|
+
|
17
|
+
headers['X-Contentful-Topic'] = "ContentfulManagement.Entry.#{event}"
|
18
|
+
request = RequestDummy.new(headers, body)
|
19
|
+
subject.respond(request, MockResponse.new).join
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
[:delete, :unpublish, :archive, :publish].each do |event|
|
24
|
+
it "deletes webhook metadata in publish queue on #{event}" do
|
25
|
+
expect(queue).to receive(:remove)
|
26
|
+
|
27
|
+
headers['X-Contentful-Topic'] = "ContentfulManagement.Entry.#{event}"
|
28
|
+
request = RequestDummy.new(headers, body)
|
29
|
+
subject.respond(request, MockResponse.new).join
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
[:create, :save, :unarchive, :delete, :unpublish, :archive, :publish].each do |event|
|
34
|
+
['Asset', 'ContentType'].each do |kind|
|
35
|
+
it "ignores #{kind} on #{event}" do
|
36
|
+
expect(queue).not_to receive(:remove)
|
37
|
+
expect(queue).not_to receive(:update_or_create)
|
38
|
+
|
39
|
+
headers['X-Contentful-Topic'] = "ContentfulManagement.#{kind}.#{event}"
|
40
|
+
request = RequestDummy.new(headers, body)
|
41
|
+
subject.respond(request, MockResponse.new).join
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class WebhookDouble
|
4
|
+
attr_reader :id, :space_id, :sys, :fields
|
5
|
+
def initialize(id, space_id, sys = {}, fields = {})
|
6
|
+
@id = id
|
7
|
+
@space_id = space_id
|
8
|
+
@sys = sys
|
9
|
+
@fields = fields
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Contentful::Scheduler::Queue do
|
14
|
+
let(:config) {
|
15
|
+
{
|
16
|
+
logger: ::Contentful::Scheduler::DEFAULT_LOGGER,
|
17
|
+
endpoint: ::Contentful::Scheduler::DEFAULT_ENDPOINT,
|
18
|
+
port: ::Contentful::Scheduler::DEFAULT_PORT,
|
19
|
+
redis: {
|
20
|
+
host: 'localhost',
|
21
|
+
port: 12341,
|
22
|
+
password: 'foobar'
|
23
|
+
},
|
24
|
+
spaces: {
|
25
|
+
'foo' => {
|
26
|
+
publish_field: 'my_field',
|
27
|
+
management_token: 'foo'
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
subject { described_class.instance }
|
34
|
+
|
35
|
+
before :each do
|
36
|
+
allow(Resque).to receive(:redis=)
|
37
|
+
described_class.class_variable_set(:@@instance, nil)
|
38
|
+
::Contentful::Scheduler.config = config
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'singleton' do
|
42
|
+
it 'creates an instance if not initialized' do
|
43
|
+
queue = described_class.instance
|
44
|
+
expect(queue).to be_a described_class
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'reuses same instance' do
|
48
|
+
queue = described_class.instance
|
49
|
+
|
50
|
+
expect(queue).to eq described_class.instance
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'attributes' do
|
55
|
+
it '.config' do
|
56
|
+
expect(subject.config).to eq config
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'instance methods' do
|
61
|
+
it '#spaces' do
|
62
|
+
expect(subject.spaces).to eq config[:spaces]
|
63
|
+
end
|
64
|
+
|
65
|
+
it '#webhook_publish_field?' do
|
66
|
+
expect(subject.webhook_publish_field?(
|
67
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => 'something'})
|
68
|
+
)).to be_truthy
|
69
|
+
|
70
|
+
expect(subject.webhook_publish_field?(
|
71
|
+
WebhookDouble.new('bar', 'foo', {}, {'not_my_field' => 'something'})
|
72
|
+
)).to be_falsey
|
73
|
+
|
74
|
+
expect(subject.webhook_publish_field?(
|
75
|
+
WebhookDouble.new('bar', 'not_foo', {}, {'not_my_field' => 'something'})
|
76
|
+
)).to be_falsey
|
77
|
+
end
|
78
|
+
|
79
|
+
it '#webhook_publish_field' do
|
80
|
+
expect(subject.webhook_publish_field(
|
81
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => 'something'})
|
82
|
+
)).to eq 'something'
|
83
|
+
end
|
84
|
+
|
85
|
+
describe '#publish_date' do
|
86
|
+
it 'works if date field not localized' do
|
87
|
+
expect(subject.publish_date(
|
88
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'})
|
89
|
+
)).to eq DateTime.new(2011, 4, 4, 22, 0, 0).to_time.utc
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'works if date field localized by grabbing first available locale' do
|
93
|
+
expect(subject.publish_date(
|
94
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => {'en-US': '2011-04-04T22:00:00+00:00'}})
|
95
|
+
)).to eq DateTime.new(2011, 4, 4, 22, 0, 0).to_time.utc
|
96
|
+
|
97
|
+
expect(subject.publish_date(
|
98
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => {'en-CA': '2011-04-04T23:00:00Z'}})
|
99
|
+
)).to eq DateTime.new(2011, 4, 4, 23, 0, 0).to_time.utc
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#already_published?' do
|
104
|
+
it 'true if webhook publish_date is in past' do
|
105
|
+
expect(subject.already_published?(
|
106
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'})
|
107
|
+
)).to be_truthy
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'true if sys.publishedAt is in past' do
|
111
|
+
expect(subject.already_published?(
|
112
|
+
WebhookDouble.new('bar', 'foo', {'publishedAt' => '2011-04-04T22:00:00+00:00'}, {'my_field' => '2099-04-04T22:00:00+00:00'})
|
113
|
+
)).to be_truthy
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'false if sys.publishedAt is not present' do
|
117
|
+
expect(subject.already_published?(
|
118
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2099-04-04T22:00:00+00:00'})
|
119
|
+
)).to be_falsey
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'false if sys.publishedAt is present but nil' do
|
123
|
+
expect(subject.already_published?(
|
124
|
+
WebhookDouble.new('bar', 'foo', {'publishedAt' => nil}, {'my_field' => '2099-04-04T22:00:00+00:00'})
|
125
|
+
)).to be_falsey
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe '#publishable?' do
|
130
|
+
it 'false if webhook space not present in config' do
|
131
|
+
expect(subject.publishable?(
|
132
|
+
WebhookDouble.new('bar', 'not_foo')
|
133
|
+
)).to be_falsey
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'false if publish_field is not found' do
|
137
|
+
expect(subject.publishable?(
|
138
|
+
WebhookDouble.new('bar', 'foo')
|
139
|
+
)).to be_falsey
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'false if publish_field is nil' do
|
143
|
+
expect(subject.publishable?(
|
144
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => nil})
|
145
|
+
)).to be_falsey
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'true if publish_field is populated' do
|
149
|
+
expect(subject.publishable?(
|
150
|
+
WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'})
|
151
|
+
)).to be_truthy
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe '#in_queue?' do
|
156
|
+
it 'false if not in queue' do
|
157
|
+
allow(Resque).to receive(:peek) { [] }
|
158
|
+
expect(subject.in_queue?(
|
159
|
+
WebhookDouble.new('bar', 'foo')
|
160
|
+
)).to be_falsey
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'true if in queue' do
|
164
|
+
allow(Resque).to receive(:peek) { [{'args' => ['foo', 'bar']}] }
|
165
|
+
expect(subject.in_queue?(
|
166
|
+
WebhookDouble.new('bar', 'foo')
|
167
|
+
)).to be_truthy
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe '#update_or_create' do
|
172
|
+
it 'does nothing if webhook is unpublishable' do
|
173
|
+
expect(Resque).not_to receive(:enqueue_at)
|
174
|
+
|
175
|
+
subject.update_or_create(WebhookDouble.new('bar', 'not_foo'))
|
176
|
+
end
|
177
|
+
|
178
|
+
describe 'webhook is new' do
|
179
|
+
it 'queues' do
|
180
|
+
mock_redis = Object.new
|
181
|
+
allow(mock_redis).to receive(:client) { mock_redis }
|
182
|
+
allow(mock_redis).to receive(:id) { 'foo' }
|
183
|
+
allow(Resque).to receive(:peek) { [] }
|
184
|
+
allow(Resque).to receive(:redis) { mock_redis }
|
185
|
+
|
186
|
+
expect(Resque).to receive(:enqueue_at).with(
|
187
|
+
DateTime.strptime('2099-04-04T22:00:00+00:00').to_time.utc,
|
188
|
+
::Contentful::Scheduler::Tasks::Publish,
|
189
|
+
'foo',
|
190
|
+
'bar',
|
191
|
+
'foo'
|
192
|
+
) { true }
|
193
|
+
|
194
|
+
subject.update_or_create(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2099-04-04T22:00:00+00:00'}))
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'does nothing if already published' do
|
198
|
+
allow(Resque).to receive(:peek) { [] }
|
199
|
+
expect(Resque).not_to receive(:enqueue_at)
|
200
|
+
|
201
|
+
subject.update_or_create(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'}))
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe 'webhook already in queue' do
|
206
|
+
it 'calls remove then queues again' do
|
207
|
+
mock_redis = Object.new
|
208
|
+
allow(mock_redis).to receive(:client) { mock_redis }
|
209
|
+
allow(mock_redis).to receive(:id) { 'foo' }
|
210
|
+
allow(Resque).to receive(:redis) { mock_redis }
|
211
|
+
|
212
|
+
allow(Resque).to receive(:peek) { [{'args' => ['foo', 'bar']}] }
|
213
|
+
expect(Resque).to receive(:enqueue_at).with(
|
214
|
+
DateTime.strptime('2099-04-04T22:00:00+00:00').to_time.utc,
|
215
|
+
::Contentful::Scheduler::Tasks::Publish,
|
216
|
+
'foo',
|
217
|
+
'bar',
|
218
|
+
'foo'
|
219
|
+
) { true }
|
220
|
+
expect(subject).to receive(:remove)
|
221
|
+
|
222
|
+
subject.update_or_create(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2099-04-04T22:00:00+00:00'}))
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'removes old call if already published' do
|
226
|
+
allow(Resque).to receive(:peek) { [{'args' => ['foo', 'bar']}] }
|
227
|
+
expect(Resque).not_to receive(:enqueue_at)
|
228
|
+
expect(subject).to receive(:remove)
|
229
|
+
|
230
|
+
subject.update_or_create(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'}))
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
describe '#remove' do
|
236
|
+
it 'does nothing if webhook is unpublishable' do
|
237
|
+
expect(Resque).not_to receive(:remove_delayed)
|
238
|
+
|
239
|
+
subject.remove(WebhookDouble.new('bar', 'foo'))
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'does nothing if webhook not in queue' do
|
243
|
+
allow(Resque).to receive(:peek) { [] }
|
244
|
+
expect(Resque).not_to receive(:remove_delayed)
|
245
|
+
|
246
|
+
subject.remove(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'}))
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'removes if in queue' do
|
250
|
+
allow(Resque).to receive(:peek) { [{'args' => ['foo', 'bar']}] }
|
251
|
+
expect(Resque).to receive(:remove_delayed)
|
252
|
+
|
253
|
+
subject.remove(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'}))
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|