rails-contact 0.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/workflows/ci.yml +65 -0
  4. data/CHANGELOG.md +10 -0
  5. data/Gemfile +19 -0
  6. data/LICENSE.txt +21 -0
  7. data/MIT-LICENSE +21 -0
  8. data/README.md +71 -0
  9. data/Rakefile +3 -0
  10. data/app/assets/stylesheets/rails/contact/application.css +15 -0
  11. data/app/controllers/rails/contact/application_controller.rb +13 -0
  12. data/app/controllers/rails/contact/contacts_controller.rb +77 -0
  13. data/app/helpers/rails/contact/application_helper.rb +6 -0
  14. data/app/jobs/rails/contact/application_job.rb +6 -0
  15. data/app/jobs/rails/contact/google_sync_job.rb +13 -0
  16. data/app/jobs/rails/contact/index_contact_job.rb +18 -0
  17. data/app/mailers/rails/contact/application_mailer.rb +8 -0
  18. data/app/mailers/rails/contact/contact_mailer.rb +32 -0
  19. data/app/models/rails/contact/application_record.rb +7 -0
  20. data/app/models/rails/contact/contact.rb +32 -0
  21. data/app/models/rails/contact/contact_address.rb +9 -0
  22. data/app/models/rails/contact/contact_email.rb +24 -0
  23. data/app/models/rails/contact/contact_phone.rb +28 -0
  24. data/app/views/layouts/rails/contact/application.html.erb +17 -0
  25. data/app/views/rails/contact/contacts/_form.html.erb +61 -0
  26. data/app/views/rails/contact/contacts/edit.html.erb +3 -0
  27. data/app/views/rails/contact/contacts/index.html.erb +38 -0
  28. data/app/views/rails/contact/contacts/new.html.erb +3 -0
  29. data/app/views/rails/contact/contacts/show.html.erb +13 -0
  30. data/bin/rails +25 -0
  31. data/bin/rubocop +8 -0
  32. data/config/routes.rb +4 -0
  33. data/lib/generators/rails/contact/contact_generator.rb +29 -0
  34. data/lib/generators/rails/contact/install/install_generator.rb +19 -0
  35. data/lib/generators/rails/contact/install/templates/rails_contact.rb.tt +16 -0
  36. data/lib/generators/rails/contact/templates/create_rails_contact_contact_addresses.rb.tt +13 -0
  37. data/lib/generators/rails/contact/templates/create_rails_contact_contact_emails.rb.tt +15 -0
  38. data/lib/generators/rails/contact/templates/create_rails_contact_contact_phones.rb.tt +16 -0
  39. data/lib/generators/rails/contact/templates/create_rails_contact_contacts.rb.tt +24 -0
  40. data/lib/generators/rails/contact/views_generator.rb +15 -0
  41. data/lib/generators/rails_contact/rails_contact_generator.rb +7 -0
  42. data/lib/rails/contact/configuration.rb +25 -0
  43. data/lib/rails/contact/csv/import_service.rb +87 -0
  44. data/lib/rails/contact/engine.rb +21 -0
  45. data/lib/rails/contact/google/client.rb +55 -0
  46. data/lib/rails/contact/google/conflict_resolver.rb +20 -0
  47. data/lib/rails/contact/google/payload_mapper.rb +26 -0
  48. data/lib/rails/contact/google/sync_service.rb +34 -0
  49. data/lib/rails/contact/google/token_store.rb +31 -0
  50. data/lib/rails/contact/orm.rb +19 -0
  51. data/lib/rails/contact/routing.rb +11 -0
  52. data/lib/rails/contact/search/backends/database.rb +41 -0
  53. data/lib/rails/contact/search/backends/elasticsearch.rb +119 -0
  54. data/lib/rails/contact/search/query.rb +27 -0
  55. data/lib/rails/contact/version.rb +5 -0
  56. data/lib/rails/contact.rb +32 -0
  57. data/lib/tasks/rails/contact_tasks.rake +31 -0
  58. data/test/csv_import_service_test.rb +25 -0
  59. data/test/google_sync_service_test.rb +47 -0
  60. data/test/payload_mapper_test.rb +19 -0
  61. data/test/search_fallback_test.rb +25 -0
  62. data/test/test_helper.rb +79 -0
  63. metadata +192 -0
@@ -0,0 +1,13 @@
1
+ <h1><%= @contact.full_name %></h1>
2
+ <p>Email: <%= @contact.primary_email&.value %></p>
3
+ <p>Phone: <%= @contact.primary_phone&.e164 %></p>
4
+ <p>Current city: <%= @contact.current_city %></p>
5
+ <p>Departure city: <%= @contact.departure_city %></p>
6
+ <p>Region: <%= @contact.region_name %></p>
7
+ <p>Biography: <%= @contact.biography %></p>
8
+
9
+ <p>
10
+ <%= link_to "Edit", edit_contact_path(@contact) %> |
11
+ <%= link_to "Delete", contact_path(@contact), data: {turbo_method: :delete, turbo_confirm: "Delete contact?"} %> |
12
+ <%= link_to "Back", contacts_path %>
13
+ </p>
data/bin/rails ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path("..", __dir__)
6
+ ENGINE_PATH = File.expand_path("../lib/rails/contact/engine", __dir__)
7
+
8
+ # Set up gems listed in the Gemfile.
9
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
10
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
11
+
12
+ require "rails"
13
+ # Pick the frameworks you want:
14
+ require "active_model/railtie"
15
+ require "active_job/railtie"
16
+ require "active_record/railtie"
17
+ # require "active_storage/engine"
18
+ require "action_controller/railtie"
19
+ require "action_mailer/railtie"
20
+ # require "action_mailbox/engine"
21
+ # require "action_text/engine"
22
+ require "action_view/railtie"
23
+ require "action_cable/engine"
24
+ # require "rails/test_unit/railtie"
25
+ require "rails/engine/commands"
data/bin/rubocop ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require "rubygems"
3
+ require "bundler/setup"
4
+
5
+ # explicit rubocop config increases performance slightly while avoiding config confusion.
6
+ ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
7
+
8
+ load Gem.bin_path("rubocop", "rubocop")
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Rails::Contact::Engine.routes.draw do
2
+ resources :contacts
3
+ root to: "contacts#index"
4
+ end
@@ -0,0 +1,29 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+
4
+ module Rails
5
+ module Contact
6
+ module Generators
7
+ class ContactGenerator < ::Rails::Generators::NamedBase
8
+ include ::Rails::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def self.next_migration_number(dirname)
13
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
14
+ end
15
+
16
+ def create_initializer
17
+ invoke "rails:contact:install"
18
+ end
19
+
20
+ def copy_migrations
21
+ migration_template "create_rails_contact_contacts.rb.tt", "db/migrate/create_rails_contact_contacts.rb"
22
+ migration_template "create_rails_contact_contact_emails.rb.tt", "db/migrate/create_rails_contact_contact_emails.rb"
23
+ migration_template "create_rails_contact_contact_phones.rb.tt", "db/migrate/create_rails_contact_contact_phones.rb"
24
+ migration_template "create_rails_contact_contact_addresses.rb.tt", "db/migrate/create_rails_contact_contact_addresses.rb"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ require "rails/generators"
2
+
3
+ module Rails
4
+ module Contact
5
+ module Generators
6
+ class InstallGenerator < ::Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def create_initializer
10
+ template "rails_contact.rb.tt", "config/initializers/rails_contact.rb"
11
+ end
12
+
13
+ def mount_engine_route
14
+ route "mount Rails::Contact::Engine => '/contacts', as: 'rails_contact'"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ Rails::Contact.configure do |config|
2
+ config.contact_class_name = "Rails::Contact::Contact"
3
+ config.search_backend = :elasticsearch
4
+ config.elasticsearch_url = ENV.fetch("ELASTICSEARCH_URL", "http://127.0.0.1:9200")
5
+
6
+ config.google_sync_enabled = ENV.fetch("RAILS_CONTACT_GOOGLE_SYNC_ENABLED", "false") == "true"
7
+ config.google_max_contacts = 25_000
8
+ config.rolling_window_sort = :updated_at
9
+
10
+ config.google_client_id = ENV["GOOGLE_CLIENT_ID"]
11
+ config.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
12
+ config.google_redirect_uri = ENV["GOOGLE_REDIRECT_URI"]
13
+ config.google_token_path = ENV.fetch("RAILS_CONTACT_GOOGLE_TOKEN_PATH", "tmp/rails_contact_google_token.json")
14
+
15
+ config.default_per_page = 25
16
+ end
@@ -0,0 +1,13 @@
1
+ class CreateRailsContactContactAddresses < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :rails_contact_contact_addresses do |t|
4
+ t.references :contact, null: false, foreign_key: {to_table: :rails_contact_contacts}
5
+ t.string :city
6
+ t.string :departure_city
7
+ t.text :formatted_value
8
+ t.string :label
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class CreateRailsContactContactEmails < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :rails_contact_contact_emails do |t|
4
+ t.references :contact, null: false, foreign_key: {to_table: :rails_contact_contacts}
5
+ t.string :value, null: false
6
+ t.string :label
7
+ t.boolean :primary, null: false, default: false
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :rails_contact_contact_emails, :value
13
+ add_index :rails_contact_contact_emails, [:contact_id, :primary]
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ class CreateRailsContactContactPhones < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :rails_contact_contact_phones do |t|
4
+ t.references :contact, null: false, foreign_key: {to_table: :rails_contact_contacts}
5
+ t.string :value, null: false
6
+ t.string :e164
7
+ t.string :label
8
+ t.boolean :primary, null: false, default: false
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :rails_contact_contact_phones, :e164
14
+ add_index :rails_contact_contact_phones, [:contact_id, :primary]
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ class CreateRailsContactContacts < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :rails_contact_contacts do |t|
4
+ t.string :given_name, null: false
5
+ t.string :family_name
6
+ t.string :region_name
7
+ t.string :departure_city
8
+ t.string :current_city
9
+ t.string :google_resource_name
10
+ t.string :google_etag
11
+ t.datetime :google_last_modified_at
12
+ t.datetime :last_synced_at
13
+ t.boolean :sync_eligible, null: false, default: true
14
+ t.text :biography
15
+ t.jsonb :metadata, null: false, default: {}
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :rails_contact_contacts, :updated_at
21
+ add_index :rails_contact_contacts, :sync_eligible
22
+ add_index :rails_contact_contacts, :google_resource_name, unique: true
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ require "rails/generators/base"
2
+
3
+ module Rails
4
+ module Contact
5
+ module Generators
6
+ class ViewsGenerator < ::Rails::Generators::Base
7
+ source_root File.expand_path("../../../../app/views", __dir__)
8
+
9
+ def copy_views
10
+ directory "rails/contact/contacts", "app/views/rails_contact/contacts"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ require "rails/generators"
2
+
3
+ class RailsContactGenerator < ::Rails::Generators::NamedBase
4
+ def delegate_to_engine_generator
5
+ invoke "rails:contact:contact", [ name ]
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ module Rails
2
+ module Contact
3
+ class Configuration
4
+ attr_accessor :contact_class_name, :elasticsearch_url, :search_backend,
5
+ :google_sync_enabled, :google_max_contacts, :rolling_window_sort,
6
+ :google_client_id, :google_client_secret, :google_redirect_uri,
7
+ :google_token_path, :reset_index_on_boot, :default_per_page
8
+
9
+ def initialize
10
+ @contact_class_name = "Rails::Contact::Contact"
11
+ @elasticsearch_url = ENV.fetch("ELASTICSEARCH_URL", "http://127.0.0.1:9200")
12
+ @search_backend = :elasticsearch
13
+ @google_sync_enabled = false
14
+ @google_max_contacts = 25_000
15
+ @rolling_window_sort = :updated_at
16
+ @google_client_id = ENV["GOOGLE_CLIENT_ID"]
17
+ @google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
18
+ @google_redirect_uri = ENV["GOOGLE_REDIRECT_URI"]
19
+ @google_token_path = ENV.fetch("RAILS_CONTACT_GOOGLE_TOKEN_PATH", "tmp/rails_contact_google_token.json")
20
+ @reset_index_on_boot = false
21
+ @default_per_page = 25
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,87 @@
1
+ require "csv"
2
+
3
+ module Rails
4
+ module Contact
5
+ module Csv
6
+ class ImportService
7
+ HEADER_MAP = {
8
+ "Enquirer First Name" => :given_name,
9
+ "Enquirer Last Name" => :family_name,
10
+ "Current City" => :current_city,
11
+ "Departure City" => :departure_city,
12
+ "Region Name" => :region_name
13
+ }.freeze
14
+
15
+ def initialize(path:, dedupe_key: :email)
16
+ @path = path
17
+ @dedupe_key = dedupe_key
18
+ end
19
+
20
+ def import!
21
+ imported = 0
22
+ CSV.foreach(@path, headers: true) do |row|
23
+ import_row(row.to_h)
24
+ imported += 1
25
+ end
26
+ imported
27
+ end
28
+
29
+ private
30
+
31
+ def import_row(row)
32
+ attrs = build_attributes(row)
33
+ contact = find_or_initialize(row)
34
+ contact.assign_attributes(attrs)
35
+ contact.save!
36
+ upsert_associations(contact, row)
37
+ Rails::Contact::IndexContactJob.perform_later(contact.id)
38
+ end
39
+
40
+ def build_attributes(row)
41
+ HEADER_MAP.each_with_object({}) do |(csv_key, attr), memo|
42
+ value = row[csv_key].to_s.strip
43
+ memo[attr] = (value == "blank" ? nil : value.presence)
44
+ end
45
+ end
46
+
47
+ def find_or_initialize(row)
48
+ email = normalize_email(row["Enquirer Email"])
49
+ return Rails::Contact::Contact.new if email.blank?
50
+ return Rails::Contact::Contact.new if @dedupe_key != :email
51
+
52
+ Rails::Contact::Contact.joins(:emails).where(rails_contact_contact_emails: { value: email }).first || Rails::Contact::Contact.new
53
+ end
54
+
55
+ def upsert_associations(contact, row)
56
+ email = normalize_email(row["Enquirer Email"])
57
+ primary_phone = normalize_phone(row["Enquirer Phone Country Code"], row["Enquirer Phone"])
58
+ alt_phone = normalize_phone(row["Alternate Wa Phone Country Code"], row["Alternate Wa Phone"])
59
+
60
+ contact.emails.find_or_create_by!(value: email, primary: true, label: "work") if email.present?
61
+ if primary_phone.present?
62
+ contact.phones.find_or_create_by!(value: primary_phone, e164: primary_phone, primary: true, label: "mobile")
63
+ end
64
+ if alt_phone.present?
65
+ contact.phones.find_or_create_by!(value: alt_phone, e164: alt_phone, primary: false, label: "whatsapp")
66
+ end
67
+
68
+ formatted_value = "Current city: #{contact.current_city}\nDeparture city: #{contact.departure_city}"
69
+ contact.addresses.find_or_create_by!(city: contact.current_city, departure_city: contact.departure_city, formatted_value: formatted_value)
70
+ end
71
+
72
+ def normalize_email(value)
73
+ clean = value.to_s.strip.downcase
74
+ clean.presence
75
+ end
76
+
77
+ def normalize_phone(country_code, number)
78
+ digits = number.to_s.gsub(/\D+/, "")
79
+ return nil if digits.blank?
80
+ prefix = country_code.to_s.strip
81
+ prefix = "+#{prefix}" unless prefix.start_with?("+")
82
+ "#{prefix}#{digits}"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,21 @@
1
+ module Rails
2
+ module Contact
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Rails::Contact
5
+
6
+ config.generators.test_framework :minitest
7
+
8
+ initializer "rails_contact.configure_defaults" do
9
+ Rails::Contact.configuration
10
+ end
11
+
12
+ initializer "rails_contact.filter_sensitive_params" do |app|
13
+ app.config.filter_parameters += %i[google_access_token google_refresh_token authorization]
14
+ end
15
+
16
+ rake_tasks do
17
+ load File.expand_path("../../tasks/rails/contact_tasks.rake", __dir__)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ require "json"
2
+
3
+ module Rails
4
+ module Contact
5
+ module Google
6
+ class Client
7
+ PEOPLE_API_BASE = "https://people.googleapis.com/v1".freeze
8
+
9
+ def initialize(access_token:)
10
+ @connection = Faraday.new(url: PEOPLE_API_BASE) do |faraday|
11
+ faraday.request :retry, max: 3, interval: 0.5, backoff_factor: 2
12
+ faraday.response :raise_error
13
+ end
14
+ @access_token = access_token
15
+ end
16
+
17
+ def create_contact(payload)
18
+ post("/people:createContact", payload)
19
+ end
20
+
21
+ def update_contact(resource_name, payload)
22
+ patch("/#{resource_name}:updateContact", payload)
23
+ end
24
+
25
+ def delete_contact(resource_name)
26
+ @connection.delete("/#{resource_name}") { |request| request.headers = headers }
27
+ end
28
+
29
+ private
30
+
31
+ def post(path, payload)
32
+ parse(@connection.post(path) do |request|
33
+ request.headers = headers
34
+ request.body = payload.to_json
35
+ end)
36
+ end
37
+
38
+ def patch(path, payload)
39
+ parse(@connection.patch(path) do |request|
40
+ request.headers = headers
41
+ request.body = payload.to_json
42
+ end)
43
+ end
44
+
45
+ def parse(response)
46
+ JSON.parse(response.body)
47
+ end
48
+
49
+ def headers
50
+ { "Authorization" => "Bearer #{@access_token}", "Content-Type" => "application/json" }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ module Rails
2
+ module Contact
3
+ module Google
4
+ class ConflictResolver
5
+ def initialize(local_contact, remote_modified_at:)
6
+ @local_contact = local_contact
7
+ @remote_modified_at = remote_modified_at
8
+ end
9
+
10
+ # v1 policy: last-write-wins by modified timestamp.
11
+ def prefer_remote?
12
+ return false if @remote_modified_at.blank?
13
+ return true if @local_contact.updated_at.blank?
14
+
15
+ @remote_modified_at > @local_contact.updated_at
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ module Rails
2
+ module Contact
3
+ module Google
4
+ class PayloadMapper
5
+ def initialize(contact)
6
+ @contact = contact
7
+ end
8
+
9
+ def to_people_payload
10
+ {
11
+ names: [ { givenName: @contact.given_name, familyName: @contact.family_name } ],
12
+ emailAddresses: @contact.emails.map { |email| { value: email.value, type: (email.label || "other") } },
13
+ phoneNumbers: @contact.phones.map { |phone| { value: phone.e164.presence || phone.value, type: (phone.label || "mobile") } },
14
+ addresses: @contact.addresses.map do |address|
15
+ {
16
+ city: address.city,
17
+ formattedValue: address.formatted_value.presence || "Current city: #{@contact.current_city}\nDeparture city: #{@contact.departure_city}"
18
+ }
19
+ end,
20
+ biographies: [ { value: [ @contact.biography, @contact.region_name ].compact.join("\n") } ]
21
+ }.compact
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module Rails
2
+ module Contact
3
+ module Google
4
+ class SyncService
5
+ def initialize(token_store: TokenStore.new, client: nil)
6
+ @token_store = token_store
7
+ @client = client || Client.new(access_token: @token_store.access_token)
8
+ end
9
+
10
+ def sync!
11
+ Rails::Contact::Contact.sync_window.each do |contact|
12
+ sync_contact(contact)
13
+ end
14
+ end
15
+
16
+ def sync_contact(contact)
17
+ payload = PayloadMapper.new(contact).to_people_payload
18
+ response = if contact.google_resource_name.present?
19
+ @client.update_contact(contact.google_resource_name, payload)
20
+ else
21
+ @client.create_contact(payload)
22
+ end
23
+
24
+ contact.update!(
25
+ google_resource_name: response["resourceName"] || contact.google_resource_name,
26
+ google_etag: response["etag"] || contact.google_etag,
27
+ google_last_modified_at: Time.current,
28
+ last_synced_at: Time.current
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ require "json"
2
+ require "fileutils"
3
+
4
+ module Rails
5
+ module Contact
6
+ module Google
7
+ class TokenStore
8
+ def initialize(path: Rails::Contact.configuration.google_token_path)
9
+ @path = path
10
+ end
11
+
12
+ def access_token
13
+ data.fetch("access_token")
14
+ end
15
+
16
+ def write!(payload)
17
+ FileUtils.mkdir_p(File.dirname(@path))
18
+ File.write(@path, payload.to_json)
19
+ end
20
+
21
+ private
22
+
23
+ def data
24
+ @data ||= JSON.parse(File.read(@path))
25
+ rescue Errno::ENOENT
26
+ raise Rails::Contact::Error, "Google token file missing at #{@path}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ module Rails
2
+ module Contact
3
+ module ORM
4
+ module_function
5
+
6
+ def adapter
7
+ ENV.fetch("DEVISE_ORM", "active_record")
8
+ end
9
+
10
+ def active_record?
11
+ adapter == "active_record"
12
+ end
13
+
14
+ def mongoid?
15
+ adapter == "mongoid"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module Rails
2
+ module Contact
3
+ module Routing
4
+ def rails_contact_for(path = :contacts)
5
+ mount Rails::Contact::Engine => "/#{path}", as: "rails_contact"
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ ActionDispatch::Routing::Mapper.include(Rails::Contact::Routing)
@@ -0,0 +1,41 @@
1
+ module Rails
2
+ module Contact
3
+ module Search
4
+ module Backends
5
+ class Database
6
+ def search(query, filters)
7
+ scope = Contact.includes(:emails, :phones).recent_first
8
+ scope = apply_filters(scope, filters)
9
+ return scope.limit(limit) if query.blank?
10
+
11
+ wildcard = "%#{query.downcase}%"
12
+ scope.left_joins(:emails, :phones).where(
13
+ "LOWER(rails_contact_contacts.given_name) LIKE :q OR "\
14
+ "LOWER(rails_contact_contacts.family_name) LIKE :q OR "\
15
+ "LOWER(rails_contact_contact_emails.value) LIKE :q OR "\
16
+ "rails_contact_contact_phones.e164 LIKE :raw",
17
+ q: wildcard,
18
+ raw: "%#{query}%"
19
+ ).distinct.limit(limit)
20
+ end
21
+
22
+ private
23
+
24
+ def apply_filters(scope, filters)
25
+ scoped = scope
26
+ scoped = scoped.where(current_city: filters["city"]) if filters["city"].present?
27
+ scoped = scoped.where(region_name: filters["region"]) if filters["region"].present?
28
+ if filters["sync_eligible"].present?
29
+ scoped = scoped.where(sync_eligible: ActiveModel::Type::Boolean.new.cast(filters["sync_eligible"]))
30
+ end
31
+ scoped
32
+ end
33
+
34
+ def limit
35
+ Rails::Contact.configuration.default_per_page
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end