Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Messages.list #5

Merged
merged 10 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ PATH
specs:
outboxer (0.1.11)
activerecord (~> 7.0)
kaminari (~> 1.2)

GEM
remote: https://rubygems.org/
specs:
actionview (7.1.2)
activesupport (= 7.1.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activemodel (7.1.2)
activesupport (= 7.1.2)
activerecord (7.1.2)
Expand All @@ -26,10 +33,12 @@ GEM
ast (2.4.2)
base64 (0.2.0)
bigdecimal (3.1.5)
builder (3.2.4)
byebug (11.1.3)
coderay (1.1.3)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
crass (1.0.6)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.1.0)
Expand All @@ -40,16 +49,38 @@ GEM
docile (1.4.0)
drb (2.2.0)
ruby2_keywords
erubi (1.12.0)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
foreman (0.87.2)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
actionview
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
language_server-protocol (3.17.0.3)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
method_source (1.0.0)
minitest (5.20.0)
mutex_m (0.2.0)
nokogiri (1.16.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.2-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.2-x86_64-linux)
racc (~> 1.4)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
Expand All @@ -63,6 +94,13 @@ GEM
pry (>= 0.13, < 0.15)
racc (1.7.1)
rack (3.0.8)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rainbow (3.1.1)
rake (13.0.6)
redis-client (0.19.1)
Expand Down
3 changes: 3 additions & 0 deletions lib/outboxer.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
require "active_support"
require "kaminari"

require_relative "outboxer/version"
require_relative "outboxer/railtie" if defined?(Rails)

require_relative "outboxer/error"
require_relative "outboxer/argument_error"

require_relative "outboxer/logger"

require_relative "outboxer/models"
Expand Down
4 changes: 4 additions & 0 deletions lib/outboxer/argument_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Outboxer
class ArgumentError < Error
end
end
49 changes: 49 additions & 0 deletions lib/outboxer/messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,55 @@ def unpublished!(limit: 1, order: :asc)
end
end

def list(status: nil, sort: :updated_at, order: :asc, page: 1, per_page: 100)
if !status.nil? && !Models::Message::STATUSES.include?(status.to_s)
raise ArgumentError, "status must be #{Models::Message::STATUSES.join(' ')}"
end

sort_options = [:id, :status, :messageable, :created_at, :updated_at]
if !sort_options.include?(sort.to_sym)
raise ArgumentError, "sort must be #{sort_options.join(' ')}"
end

order_options = [:asc, :desc]
if !order_options.include?(order.to_sym)
raise ArgumentError, "order must be #{order_options.join(' ')}"
end

if !page.is_a?(Integer) || page <= 0
raise ArgumentError, "page must be >= 1"
end

per_page_options = [100, 200, 500, 1000]
if !per_page_options.include?(per_page)
raise ArgumentError, "per_page must be #{per_page_options.join(' ')}"
end

message_scope = Models::Message
message_scope = status.nil? ? message_scope.all : message_scope.where(status: status)

message_scope =
if sort.to_sym == :messageable
message_scope.order(messageable_type: order.to_sym, messageable_id: order.to_sym)
else
message_scope.order(sort.to_sym => order.to_sym)
end

messages = ActiveRecord::Base.connection_pool.with_connection do
message_scope.page(page).per(per_page)
end

messages.map do |message|
{
'id' => message.id,
'status' => message.status,
'messageable' => "#{message.messageable_type}::#{message.messageable_id}",
'created_at' => message.created_at.utc.to_s,
'updated_at' => message.updated_at.utc.to_s
}
end
end

def republish_all!(batch_size: 100)
updated_total_count = 0

Expand Down
1 change: 1 addition & 0 deletions outboxer.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

spec.add_dependency "activerecord", "~> 7.0"
spec.add_dependency 'kaminari', '~> 1.2'

spec.add_development_dependency 'foreman', '~> 0.87.2'
spec.add_development_dependency 'pry-byebug', '3.10'
Expand Down
123 changes: 123 additions & 0 deletions spec/lib/outboxer/messages/list/filter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
require 'spec_helper'

module Outboxer
RSpec.describe Messages do
before do
create(:outboxer_message, :unpublished, id: 4,
messageable_type: 'Event', messageable_id: 1,
created_at: 5.minutes.ago, updated_at: 4.minutes.ago)
create(:outboxer_message, :failed, id: 3,
messageable_type: 'Event', messageable_id: 2,
created_at: 4.minutes.ago, updated_at: 3.minutes.ago)
create(:outboxer_message, :publishing, id: 2,
messageable_type: 'Event', messageable_id: 3,
created_at: 3.minutes.ago, updated_at: 2.minutes.ago)
create(:outboxer_message, :unpublished, id: 1,
messageable_type: 'Event', messageable_id: 4,
created_at: 2.minutes.ago, updated_at: 1.minute.ago)
end

describe '.list' do
context 'when an invalid status is specified' do
it 'raises an ArgumentError' do
expect do
Messages.list(status: :invalid)
end.to raise_error(
ArgumentError, "status must be #{Models::Message::STATUSES.join(' ')}")
end
end

context 'with no status specified' do
it 'returns all messages' do
expect(
Messages.list(sort: :status, order: :asc)
).to match_array([
{
'id' => 3,
'status' => 'failed',
'messageable' => 'Event::2',
'created_at' => 4.minutes.ago.utc.to_s,
'updated_at' => 3.minutes.ago.utc.to_s
},
{
'id' => 2,
'status' => 'publishing',
'messageable' => 'Event::3',
'created_at' => 3.minutes.ago.utc.to_s,
'updated_at' => 2.minutes.ago.utc.to_s
},
{
'id' => 4,
'status' => 'unpublished',
'messageable' => 'Event::1',
'created_at' => 5.minutes.ago.utc.to_s,
'updated_at' => 4.minutes.ago.utc.to_s
},
{
'id' => 1,
'status' => 'unpublished',
'messageable' => 'Event::4',
'created_at' => 2.minutes.ago.utc.to_s,
'updated_at' => 1.minute.ago.utc.to_s
}
])
end
end

context 'with unpublished status' do
it 'returns unpublished messages' do
expect(
Messages.list(status: :unpublished, sort: :status, order: :asc)
).to match_array([
{
'id' => 4,
'status' => 'unpublished',
'messageable' => 'Event::1',
'created_at' => 5.minutes.ago.utc.to_s,
'updated_at' => 4.minutes.ago.utc.to_s
},
{
'id' => 1,
'status' => 'unpublished',
'messageable' => 'Event::4',
'created_at' => 2.minutes.ago.utc.to_s,
'updated_at' => 1.minute.ago.utc.to_s
}
])
end
end

context 'with publishing status' do
it 'returns publishing messages' do
expect(
Messages.list(status: :publishing, sort: :status, order: :asc)
).to match_array([
{
'id' => 2,
'status' => 'publishing',
'messageable' => 'Event::3',
'created_at' => 3.minutes.ago.utc.to_s,
'updated_at' => 2.minutes.ago.utc.to_s
}
])
end
end

context 'with failed status' do
it 'returns failed messages' do
expect(
Messages.list(status: :failed, sort: :status, order: :asc)
).to match_array([
{
'id' => 3,
'status' => 'failed',
'messageable' => 'Event::2',
'created_at' => 4.minutes.ago.utc.to_s,
'updated_at' => 3.minutes.ago.utc.to_s
}
])
end
end
end
end
end
31 changes: 31 additions & 0 deletions spec/lib/outboxer/messages/list/no_arguments_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'spec_helper'

module Outboxer
RSpec.describe Messages do
describe '.list' do
let!(:messages) do
[
create(:outboxer_message, :unpublished,
id: 1, messageable_type: 'Event', messageable_id: 1),
create(:outboxer_message, :failed,
id: 2, messageable_type: 'Event', messageable_id: 2),
create(:outboxer_message, :publishing,
id: 3, messageable_type: 'Event', messageable_id: 3),
create(:outboxer_message, :unpublished,
id: 4, messageable_type: 'Event', messageable_id: 4)
]
end

it 'returns all messages ordered by last updated at asc' do
expect(
Messages.list.map { |message| message.slice('id', 'status', 'messageable') }
).to match_array([
{ 'id' => 1, 'status' => 'unpublished', 'messageable' => 'Event::1' },
{ 'id' => 2, 'status' => 'failed', 'messageable' => 'Event::2' },
{ 'id' => 3, 'status' => 'publishing', 'messageable' => 'Event::3' },
{ 'id' => 4, 'status' => 'unpublished', 'messageable' => 'Event::4' },
])
end
end
end
end
47 changes: 47 additions & 0 deletions spec/lib/outboxer/messages/list/pagination_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'spec_helper'

module Outboxer
RSpec.describe Messages do
before do
create_list(:outboxer_message, 101, status: :unpublished,
messageable_type: 'Event', messageable_id: 1)
end

describe '.list' do
let(:first_page) { Messages.list(page: 1, per_page: 100) }
let(:second_page) { Messages.list(page: 2, per_page: 100) }

context 'when invalid per page is specified' do
it 'raises an ArgumentError' do
expect do
Messages.list(per_page: 1)
end.to raise_error(ArgumentError, "per_page must be 100 200 500 1000")
end
end

context 'when invalid page is specified' do
it 'raises an ArgumentError' do
expect do
Messages.list(page: 'hello')
end.to raise_error(ArgumentError, "page must be >= 1")
end
end

context 'when 100 messages per page' do
it 'first page returns 100 messages' do
expect(first_page.size).to eq(100)
end

it 'second page returns 1 message' do
expect(second_page.size).to eq(1)
end

it 'ensures no overlap between first and second page' do
first_page_ids = first_page.map { |message| message['id'] }
second_page_ids = second_page.map { |message| message['id'] }
expect(first_page_ids & second_page_ids).to be_empty
end
end
end
end
end
Loading
Loading