audiences 2.0.0 → 2.0.1

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.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ class ApplicationEvent < AetherObservatory::EventBase
5
+ event_prefix "audiences"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Audiences
4
+ class PersistedResourceEvent < ApplicationEvent
5
+ event_name { "persisted.#{resource_type}" }
6
+
7
+ attribute :params
8
+ attribute :resource_type
9
+ end
10
+ end
@@ -79,37 +79,22 @@ module Audiences
79
79
  "title" => names["Titles"],
80
80
  "urn:ietf:params:scim:schemas:extension:authservice:2.0:User" => {
81
81
  "role" => names["Roles"], "department" => names["Departments"],
82
- "territory" => names["Territories"], "territoryAbbr" => TERRITORY_ABBRS[names["Territories"]]
82
+ "territory" => names["Territories"], "territoryAbbr" => territory_abbr(names["Territories"])
83
83
  },
84
84
  }
85
85
  end
86
86
 
87
- TERRITORY_ABBRS = {
88
- "Philadelphia" => "PHL",
89
- "New Jersey" => "NJ",
90
- "Maryland" => "MD",
91
- "Connecticut" => "CT",
92
- "Long Island" => "LI",
93
- "Boston" => "BOS",
94
- "Atlanta" => "ATL",
95
- "Chicago" => "CHI",
96
- "Detroit" => "DET",
97
- "Houston" => "HOU",
98
- "Dallas" => "DAL",
99
- "Denver" => "DEN",
100
- "Tampa" => "TPA",
101
- "Austin" => "AUS",
102
- "Charlotte" => "CLT",
103
- "Nashville" => "NSH",
104
- "Phoenix" => "PHX",
105
- "Pittsburgh" => "PIT",
106
- "San Antonio" => "SAO",
107
- "Fort Lauderdale" => "FLL",
108
- "Las Vegas" => "LVS",
109
- "Orlando" => "ORL",
110
- "Cincinnati" => "CIN",
111
- "Columbus" => "CLB",
112
- "Jacksonville" => "JAX",
113
- }.freeze
87
+ def missing_group_types(expected_types = Audiences.config.required_group_types)
88
+ return [] if expected_types.blank?
89
+
90
+ actual_types = groups.map(&:resource_type)
91
+ expected_types - actual_types
92
+ end
93
+
94
+ private
95
+
96
+ def territory_abbr(territory)
97
+ Audiences.config.territory_abbreviations[territory]
98
+ end
114
99
  end
115
100
  end
@@ -5,6 +5,8 @@ module Audiences
5
5
  belongs_to :external_user
6
6
  belongs_to :group
7
7
 
8
+ validates :external_user_id, uniqueness: { scope: :group_id }
9
+
8
10
  after_commit on: %i[create destroy] do
9
11
  relevant_groups = Audiences::Context.relevant_to(group)
10
12
 
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddUniqueIndexToGroupMemberships < ActiveRecord::Migration[6.1]
4
+ def change
5
+ add_index :audiences_group_memberships,
6
+ %i[group_id external_user_id],
7
+ unique: true,
8
+ name: "index_group_memberships_on_group_and_user"
9
+ end
10
+ end
data/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Unreleased
2
2
 
3
+ # Version 2.0.1 (2026-05-08)
4
+
5
+ - Fixes a performance issue where we load all users when updating memberships [#557](https://github.com/powerhome/audiences/pull/557)
6
+
3
7
  # Version 2.0 (2025-08-25)
4
8
 
5
9
  The all new 2.0 release inverts the SCIM logic, where now Audiences no longer pulls data from SCIM, but rather it will receive and cache SCIM data, allowing for an in database calculation of audiences. This improved process allows audiences to be more independent from SCIM, while still compatible with the protocol.
@@ -16,6 +16,23 @@ module Audiences
16
16
  %w[Groups]
17
17
  end
18
18
 
19
+ # Group types that must be present in a user provisioning event
20
+ config_accessor :required_group_types do
21
+ []
22
+ end
23
+
24
+ DEFAULT_TERRITORY_ABBREVIATIONS = {
25
+ "Philadelphia" => "PHL", "New Jersey" => "NJ", "Maryland" => "MD", "Connecticut" => "CT",
26
+ "Long Island" => "LI", "Boston" => "BOS", "Atlanta" => "ATL", "Chicago" => "CHI",
27
+ "Detroit" => "DET", "Houston" => "HOU", "Dallas" => "DAL", "Denver" => "DEN", "Tampa" => "TPA",
28
+ "Austin" => "AUS", "Charlotte" => "CLT", "Nashville" => "NSH", "Phoenix" => "PHX",
29
+ "Pittsburgh" => "PIT", "San Antonio" => "SAO", "Fort Lauderdale" => "FLL", "Las Vegas" => "LVS",
30
+ "Orlando" => "ORL", "Cincinnati" => "CIN", "Columbus" => "CLB", "Jacksonville" => "JAX",
31
+ "Oklahoma City" => "OKC", "Raleigh" => "RLD", "Cleveland" => "CLE"
32
+ }.freeze
33
+
34
+ config_accessor(:territory_abbreviations) { DEFAULT_TERRITORY_ABBREVIATIONS }
35
+
19
36
  # Defines a default scope for users, so the users that are part of an audience can
20
37
  # be filtered (i.e.: only active, only users in a specific group, etc)
21
38
  #
@@ -98,7 +115,7 @@ module Audiences
98
115
  Audiences.logger.warn(<<~MESSAGE)
99
116
  Audiences authenticate is currently configured using a default and is blocking authenticaiton.
100
117
 
101
- To make this wraning go away provide a configuration for `Audiences.config.authenticate`.
118
+ To make this warning go away provide a configuration for `Audiences.config.authenticate`.
102
119
 
103
120
  The value should:
104
121
  1. Be callable like a Proc.
@@ -8,13 +8,27 @@ module Audiences
8
8
  end
9
9
 
10
10
  def remove(object, path, val)
11
- current = object.send to(path)
12
- _set object, path, current - value(path, val)
11
+ return unless @map.key?(path)
12
+
13
+ case @map[path]
14
+ in { to: to, find: find }
15
+ remove_from_association(object, to, find, val)
16
+ else
17
+ current = object.send to(path)
18
+ _set object, path, current - value(path, val)
19
+ end
13
20
  end
14
21
 
15
22
  def add(object, path, val)
16
- current = object.send to(path)
17
- _set object, path, current + value(path, val)
23
+ return unless @map.key?(path)
24
+
25
+ case @map[path]
26
+ in { to: to, find: find }
27
+ add_to_association(object, to, find, val)
28
+ else
29
+ current = object.send to(path)
30
+ _set object, path, current + value(path, val)
31
+ end
18
32
  end
19
33
 
20
34
  def replace(object, path, val)
@@ -44,6 +58,20 @@ module Audiences
44
58
  else val
45
59
  end
46
60
  end
61
+
62
+ def add_to_association(object, to, find, val)
63
+ # Use << operator to avoid loading all records
64
+ collection = object.send(to)
65
+ new_items = [val].flatten.pluck("value").filter_map(&find)
66
+ new_items.each { |item| collection << item unless collection.include?(item) }
67
+ end
68
+
69
+ def remove_from_association(object, to, find, val)
70
+ # Use delete operator to avoid loading all records
71
+ collection = object.send(to)
72
+ items_to_remove = [val].flatten.pluck("value").filter_map(&find)
73
+ collection.delete(*items_to_remove)
74
+ end
47
75
  end
48
76
  end
49
77
  end
@@ -12,6 +12,7 @@ module Audiences
12
12
  Audiences.logger.info "#{upsert_action} group #{new_display_name} (#{new_external_id})"
13
13
 
14
14
  group.update! external_id: new_external_id, display_name: new_display_name, active: new_active
15
+ Audiences::PersistedResourceEvent.create(resource_type: "Groups", params: event_payload.params)
15
16
  rescue => e
16
17
  Audiences.logger.error e
17
18
  raise
@@ -7,9 +7,11 @@ module Audiences
7
7
  subscribe_to "two_percent.scim.replace.Users"
8
8
 
9
9
  def process
10
- Audiences.logger.info "#{upsert_action} group #{event_payload.params['displayName']} (#{scim_id})"
11
-
10
+ log_upsert_action
12
11
  external_user.update! updated_attributes
12
+ return unless valid_group_types?
13
+
14
+ Audiences::PersistedResourceEvent.create(resource_type: "Users", params: event_payload.params)
13
15
  rescue => e
14
16
  Audiences.logger.error e
15
17
  raise
@@ -17,6 +19,10 @@ module Audiences
17
19
 
18
20
  private
19
21
 
22
+ def log_upsert_action
23
+ Audiences.logger.info "#{upsert_action} user #{event_payload.params['displayName']} (#{scim_id})"
24
+ end
25
+
20
26
  def scim_id = event_payload.params["id"]
21
27
 
22
28
  def external_user
@@ -43,6 +49,14 @@ module Audiences
43
49
  Audiences::Group.find_by(scim_id: group["value"])
44
50
  end
45
51
  end
52
+
53
+ def valid_group_types?
54
+ missing = external_user.missing_group_types
55
+ return true if missing.empty?
56
+
57
+ Audiences.logger.warn "Provisioning event for user #{scim_id} with missing group types: #{missing.join(', ')}"
58
+ false
59
+ end
46
60
  end
47
61
  end
48
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Audiences
4
- VERSION = "2.0.0"
4
+ VERSION = "2.0.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: audiences
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Palhares
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-25 00:00:00.000000000 Z
11
+ date: 2026-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aether_observatory
@@ -51,6 +51,8 @@ files:
51
51
  - app/controllers/audiences/application_controller.rb
52
52
  - app/controllers/audiences/contexts_controller.rb
53
53
  - app/controllers/audiences/scim_proxy_controller.rb
54
+ - app/events/audiences/application_event.rb
55
+ - app/events/audiences/persisted_resource_event.rb
54
56
  - app/models/audiences/application_record.rb
55
57
  - app/models/audiences/context.rb
56
58
  - app/models/audiences/context/locating.rb
@@ -85,6 +87,7 @@ files:
85
87
  - db/migrate/20250624171247_create_audiences_context_extra_users.rb
86
88
  - db/migrate/20250624171706_rename_audiences_context_extra_users_to_extra_users_json.rb
87
89
  - db/migrate/20250701173946_move_extra_users_to_context_extra_users.rb
90
+ - db/migrate/20260506150000_add_unique_index_to_group_memberships.rb
88
91
  - docs/CHANGELOG.md
89
92
  - docs/README.md
90
93
  - lib/audiences.rb