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

feat: add pagination and filtering for integrations endpoint #622

Merged
merged 3 commits into from Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/pact_broker/api/decorators/integration_decorator.rb
Expand Up @@ -40,3 +40,4 @@ class IntegrationDecorator < BaseDecorator
end
end
end

3 changes: 3 additions & 0 deletions lib/pact_broker/api/decorators/integrations_decorator.rb
@@ -1,5 +1,6 @@
require_relative "base_decorator"
require_relative "integration_decorator"
require "pact_broker/api/decorators/pagination_links"

module PactBroker
module Api
Expand All @@ -13,6 +14,8 @@ class IntegrationsDecorator < BaseDecorator
title: "All integrations"
}
end

include PactBroker::Api::Decorators::PaginationLinks
end
end
end
Expand Down
15 changes: 15 additions & 0 deletions lib/pact_broker/api/resources/filter_methods.rb
@@ -0,0 +1,15 @@
module PactBroker
module Api
module Resources
module FilterMethods
def filter_options
if request.query.has_key?("q")
{ query_string: request.query["q"] }
else
{}
end
end
end
end
end
end
22 changes: 18 additions & 4 deletions lib/pact_broker/api/resources/integrations.rb
@@ -1,11 +1,17 @@
require "pact_broker/api/resources/base_resource"
require "pact_broker/api/renderers/integrations_dot_renderer"
require "pact_broker/api/decorators/integrations_decorator"
require "pact_broker/api/resources/filter_methods"
require "pact_broker/api/resources/pagination_methods"
require "pact_broker/api/contracts/pagination_query_params_schema"

module PactBroker
module Api
module Resources
class Integrations < BaseResource
include PaginationMethods
include FilterMethods

def content_types_provided
[
["text/vnd.graphviz", :to_dot],
Expand All @@ -17,18 +23,20 @@ def allowed_methods
["GET", "OPTIONS", "DELETE"]
end

def malformed_request?
super || (request.get? && validation_errors_for_schema?(schema, request.query))
end

def to_dot
integrations = integration_service.find_all(filter_options, pagination_options)
PactBroker::Api::Renderers::IntegrationsDotRenderer.call(integrations)
end

def to_json
integrations = integration_service.find_all(filter_options, pagination_options, decorator_class(:integrations_decorator).eager_load_associations)
decorator_class(:integrations_decorator).new(integrations).to_json(**decorator_options)
end

def integrations
@integrations ||= integration_service.find_all
end

def delete_resource
integration_service.delete_all
true
Expand All @@ -37,6 +45,12 @@ def delete_resource
def policy_name
:'integrations::integrations'
end

def schema
if request.get?
PactBroker::Api::Contracts::PaginationQueryParamsSchema
end
end
end
end
end
Expand Down
15 changes: 15 additions & 0 deletions lib/pact_broker/dataset.rb
Expand Up @@ -5,6 +5,14 @@

module PactBroker
module Dataset

# Return a dataset that only includes the rows where the specified column
# includes the given query string.
# @return [Sequel::Dataset]
def filter(column_name, query_string)
where(Sequel.ilike(column_name, "%" + escape_wildcards(query_string) + "%"))
end

def name_like column_name, value
if PactBroker.configuration.use_case_sensitive_resource_names
if mysql?
Expand Down Expand Up @@ -80,5 +88,12 @@ def mysql?
def postgres?
Sequel::Model.db.adapter_scheme.to_s =~ /postgres/
end

def escape_wildcards(value)
value.gsub("_", "\\_").gsub("%", "\\%")
end

private :escape_wildcards

end
end
10 changes: 10 additions & 0 deletions lib/pact_broker/integrations/integration.rb
Expand Up @@ -78,6 +78,12 @@ class Integration < Sequel::Model(Sequel::Model.db[:integrations].select(:id, :c
dataset_module do
include PactBroker::Dataset

def filter_by_pacticipant(query_string)
matching_pacticipants = PactBroker::Domain::Pacticipant.filter(:name, query_string)
pacticipants_join = Sequel.|({ Sequel[:integrations][:consumer_id] => Sequel[:p][:id] }, { Sequel[:integrations][:provider_id] => Sequel[:p][:id] })
join(matching_pacticipants, pacticipants_join, table_alias: :p)
end

def including_pacticipant_id(pacticipant_id)
where(consumer_id: pacticipant_id).or(provider_id: pacticipant_id)
end
Expand Down Expand Up @@ -123,6 +129,10 @@ def provider_name
def pacticipant_ids
[consumer_id, provider_id]
end

def to_s
"Integration: consumer #{associations[:consumer]&.name || consumer_id}/provider #{associations[:provider]&.name || provider_id}"
end
end
end
end
Expand Down
13 changes: 13 additions & 0 deletions lib/pact_broker/integrations/repository.rb
@@ -1,8 +1,21 @@
require "pact_broker/integrations/integration"
require "pact_broker/repositories/scopes"

module PactBroker
module Integrations
class Repository

include PactBroker::Repositories::Scopes

def find(filter_options = {}, pagination_options = {}, eager_load_associations = [])
query = scope_for(PactBroker::Integrations::Integration).select_all_qualified
query = query.filter_by_pacticipant(filter_options[:query_string]) if filter_options[:query_string]
query
.eager(*eager_load_associations)
.order(Sequel.desc(:contract_data_updated_at))
.all_with_pagination_options(pagination_options)
end

def create_for_pact(consumer_id, provider_id)
if Integration.where(consumer_id: consumer_id, provider_id: provider_id).empty?
Integration.new(
Expand Down
18 changes: 2 additions & 16 deletions lib/pact_broker/integrations/service.rb
Expand Up @@ -14,22 +14,8 @@ class Service
include PactBroker::Logging
extend PactBroker::Repositories::Scopes

def self.find_all
# The only reason the pact_version needs to be loaded is that
# the Verification::PseudoBranchStatus uses it to determine if
# the pseudo branch is 'stale'.
# Because this is the status for a pact, and not a pseudo branch,
# the status can never be 'stale',
# so it would be better to create a Verification::PactStatus class
# that doesn't have the 'stale' logic in it.
# Then we can remove the eager loading of the pact_version
scope_for(PactBroker::Integrations::Integration)
.eager(:consumer)
.eager(:provider)
.eager(:latest_pact) # latest_pact eager loader is custom, can't take any more options
.eager(:latest_verification)
.all
.sort { | a, b| Integration.compare_by_last_action_date(a, b) }
def self.find_all(filter_options = {}, pagination_options = {}, eager_load_associations = [])
integration_repository.find(filter_options, pagination_options, eager_load_associations)
end

# Callback to invoke when a consumer contract, verification result (or provider contract in Pactflow) is published
Expand Down
2 changes: 1 addition & 1 deletion lib/pact_broker/pacticipants/repository.rb
Expand Up @@ -40,7 +40,7 @@ def find_all(options = {}, pagination_options = {}, eager_load_associations = []

def find(options = {}, pagination_options = {}, eager_load_associations = [])
query = scope_for(PactBroker::Domain::Pacticipant).select_all_qualified
query = query.where(Sequel.ilike(:name, "%#{options[:query_string].gsub("_", "\\_")}%")) if options[:query_string]
query = query.filter(:name, options[:query_string]) if options[:query_string]
query = query.label(options[:label_name]) if options[:label_name]
query.order_ignore_case(Sequel[:pacticipants][:name]).eager(*eager_load_associations).all_with_pagination_options(pagination_options)
end
Expand Down
44 changes: 37 additions & 7 deletions spec/features/get_integrations_spec.rb
@@ -1,17 +1,47 @@
describe "Get integrations dot file" do
describe "Get integrations" do
before do
td.create_pact_with_hierarchy("Foo", "1", "Bar")
.create_verification(provider_version: "2")
td.create_consumer("Foo")
.create_provider("Bar")
.create_integration
.create_consumer("Apple")
.create_provider("Pear")
.create_integration
.create_consumer("Dog")
.create_provider("Cat")
.create_integration
end

let(:path) { "/integrations" }
let(:response_body_hash) { JSON.parse(subject.body, symbolize_names: true) }

subject { get path, nil, {"HTTP_ACCEPT" => "application/hal+json" } }
let(:query) { nil }
let(:response_body_hash) { JSON.parse(subject.body) }
subject { get path, query, {"HTTP_ACCEPT" => "application/hal+json" } }

it { is_expected.to be_a_hal_json_success_response }

it "returns a json body with embedded integrations" do
expect(JSON.parse(subject.body)["_embedded"]["integrations"]).to be_a(Array)
expect(response_body_hash["_embedded"]["integrations"]).to be_a(Array)
end

context "with pagination options" do
let(:query) { { "pageSize" => "2", "pageNumber" => "1" } }

it_behaves_like "a paginated response"
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


context "with a query string" do
let(:query) { { "q" => "pp" } }

it "returns only the integrations with a consumer or provider name including the given string" do
expect(response_body_hash["_embedded"]["integrations"]).to contain_exactly(hash_including("consumer" => hash_including("name" => "Apple")))
end
end

context "as a dot file" do
subject { get path, query, {"HTTP_ACCEPT" => "text/vnd.graphviz" } }

it "returns a dot file" do
expect(subject.body).to include "digraph"
expect(subject.body).to include "Foo -> Bar"
end
end
end
59 changes: 59 additions & 0 deletions spec/lib/pact_broker/api/resources/integrations_spec.rb
@@ -0,0 +1,59 @@
require "pact_broker/api/resources/integrations"

module PactBroker
module Api
module Resources
describe Integrations do
describe "GET" do
before do
allow_any_instance_of(described_class).to receive(:integration_service).and_return(integration_service)
allow(integration_service).to receive(:find_all).and_return(integrations)
allow_any_instance_of(described_class).to receive(:decorator_class).and_return(decorator_class)
allow_any_instance_of(described_class).to receive_message_chain(:decorator_class, :eager_load_associations).and_return(eager_load_associations)
allow(PactBroker::Api::Contracts::PaginationQueryParamsSchema).to receive(:call).and_return(errors)
end

let(:integration_service) { class_double("PactBroker::Integrations::Service").as_stubbed_const }
let(:integrations) { double("integrations") }
let(:decorator_class) { double("decorator class", new: decorator) }
let(:decorator) { double("decorator", to_json: json) }
let(:json) { "some json" }
let(:rack_headers) { { "HTTP_ACCEPT" => "application/hal+json" } }
let(:eager_load_associations) { [:foo, :bar] }
let(:errors) { {} }

let(:path) { "/integrations" }
let(:params) { { "pageNumber" => "1", "pageSize" => "2" } }

subject { get(path, params, rack_headers) }


it "validates the query params" do
expect(PactBroker::Api::Contracts::PaginationQueryParamsSchema).to receive(:call).with(params)
subject
end

it "finds the integrations" do
allow(integration_service).to receive(:find_all).with({}, { page_number: 1, page_size: 2 }, eager_load_associations)
subject
end

its(:status) { is_expected.to eq 200 }

it "renders the integrations" do
expect(decorator_class).to receive(:new).with(integrations)
expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext))
expect(subject.body).to eq json
end

context "with invalid query params" do
let(:errors) { { "some" => ["errors"]} }

its(:status) { is_expected.to eq 400 }
its(:body) { is_expected.to match "some.*errors" }
end
end
end
end
end
end
39 changes: 39 additions & 0 deletions spec/lib/pact_broker/integrations/integration_spec.rb
Expand Up @@ -3,6 +3,45 @@
module PactBroker
module Integrations
describe Integration do
describe "filter" do
before do
td.create_consumer("Foo")
.create_provider("Bar")
.create_integration
.create_consumer("Cat")
.create_provider("Dog")
.create_integration
.create_consumer("Y")
.create_provider("Z")
.create_integration
end

subject { Integration.select_all_qualified.filter_by_pacticipant(query_string).all }

context "with a filter matching the consumer" do
let(:query_string) { "oo" }

it { is_expected.to contain_exactly(have_attributes(consumer_name: "Foo", provider_name: "Bar")) }
end

context "with a filter matching the provider" do
let(:query_string) { "ar" }

it { is_expected.to contain_exactly(have_attributes(consumer_name: "Foo", provider_name: "Bar")) }
end

context "with a filter matching both consumer and provider" do
let(:query_string) { "o" }

it "returns the matching integrations" do
expect(subject).to contain_exactly(
have_attributes(consumer_name: "Foo", provider_name: "Bar"),
have_attributes(consumer_name: "Cat", provider_name: "Dog")
)
end
end
end

describe "relationships" do
before do
td.set_now(DateTime.new(2018, 1, 7))
Expand Down