ahoy_matey 2.0.0 → 3.2.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 +5 -5
- data/CHANGELOG.md +112 -37
- data/CONTRIBUTING.md +9 -7
- data/LICENSE.txt +1 -1
- data/README.md +377 -63
- data/app/controllers/ahoy/base_controller.rb +14 -10
- data/app/controllers/ahoy/events_controller.rb +1 -1
- data/app/controllers/ahoy/visits_controller.rb +1 -0
- data/app/jobs/ahoy/geocode_v2_job.rb +3 -4
- data/lib/ahoy.rb +57 -2
- data/lib/ahoy/base_store.rb +32 -3
- data/lib/ahoy/controller.rb +21 -8
- data/lib/ahoy/database_store.rb +33 -16
- data/lib/ahoy/engine.rb +3 -1
- data/lib/ahoy/helper.rb +40 -0
- data/lib/ahoy/model.rb +2 -2
- data/lib/ahoy/query_methods.rb +46 -1
- data/lib/ahoy/tracker.rb +59 -27
- data/lib/ahoy/utils.rb +7 -0
- data/lib/ahoy/version.rb +1 -1
- data/lib/ahoy/visit_properties.rb +73 -37
- data/lib/generators/ahoy/activerecord_generator.rb +17 -26
- data/lib/generators/ahoy/base_generator.rb +1 -1
- data/lib/generators/ahoy/install_generator.rb +1 -1
- data/lib/generators/ahoy/mongoid_generator.rb +1 -5
- data/lib/generators/ahoy/templates/active_record_event_model.rb.tt +10 -0
- data/lib/generators/ahoy/templates/{active_record_migration.rb → active_record_migration.rb.tt} +14 -7
- data/lib/generators/ahoy/templates/active_record_visit_model.rb.tt +6 -0
- data/lib/generators/ahoy/templates/{base_store_initializer.rb → base_store_initializer.rb.tt} +8 -0
- data/lib/generators/ahoy/templates/database_store_initializer.rb.tt +10 -0
- data/lib/generators/ahoy/templates/{mongoid_event_model.rb → mongoid_event_model.rb.tt} +1 -1
- data/lib/generators/ahoy/templates/{mongoid_visit_model.rb → mongoid_visit_model.rb.tt} +9 -7
- data/vendor/assets/javascripts/ahoy.js +539 -552
- metadata +27 -204
- data/.github/ISSUE_TEMPLATE.md +0 -7
- data/.gitignore +0 -17
- data/Gemfile +0 -6
- data/Rakefile +0 -9
- data/ahoy_matey.gemspec +0 -36
- data/docs/Ahoy-2-Upgrade.md +0 -147
- data/docs/Data-Store-Examples.md +0 -240
- data/lib/generators/ahoy/templates/active_record_event_model.rb +0 -10
- data/lib/generators/ahoy/templates/active_record_visit_model.rb +0 -6
- data/lib/generators/ahoy/templates/database_store_initializer.rb +0 -5
- data/test/query_methods/mongoid_test.rb +0 -23
- data/test/query_methods/mysql_json_test.rb +0 -18
- data/test/query_methods/mysql_text_test.rb +0 -19
- data/test/query_methods/postgresql_hstore_test.rb +0 -20
- data/test/query_methods/postgresql_json_test.rb +0 -18
- data/test/query_methods/postgresql_jsonb_test.rb +0 -19
- data/test/query_methods/postgresql_text_test.rb +0 -19
- data/test/test_helper.rb +0 -100
@@ -1,15 +1,12 @@
|
|
1
1
|
module Ahoy
|
2
2
|
class BaseController < ApplicationController
|
3
3
|
filters = _process_action_callbacks.map(&:filter) - Ahoy.preserve_callbacks
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
skip_action_callback *filters
|
11
|
-
before_action :verify_request_size
|
12
|
-
end
|
4
|
+
skip_before_action(*filters, raise: false)
|
5
|
+
skip_after_action(*filters, raise: false)
|
6
|
+
skip_around_action(*filters, raise: false)
|
7
|
+
|
8
|
+
before_action :verify_request_size
|
9
|
+
before_action :renew_cookies
|
13
10
|
|
14
11
|
if respond_to?(:protect_from_forgery)
|
15
12
|
protect_from_forgery with: :null_session, if: -> { Ahoy.protect_from_forgery }
|
@@ -21,10 +18,17 @@ module Ahoy
|
|
21
18
|
@ahoy ||= Ahoy::Tracker.new(controller: self, api: true)
|
22
19
|
end
|
23
20
|
|
21
|
+
# set proper ttl if cookie generated from JavaScript
|
22
|
+
# approach is not perfect, as user must reload the page
|
23
|
+
# for new cookie settings to take effect
|
24
|
+
def renew_cookies
|
25
|
+
set_ahoy_cookies if params[:js] && !Ahoy.api_only
|
26
|
+
end
|
27
|
+
|
24
28
|
def verify_request_size
|
25
29
|
if request.content_length > Ahoy.max_content_length
|
26
30
|
logger.info "[ahoy] Payload too large"
|
27
|
-
render
|
31
|
+
render plain: "Payload too large\n", status: 413
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
@@ -7,14 +7,13 @@ module Ahoy
|
|
7
7
|
begin
|
8
8
|
Geocoder.search(ip).first
|
9
9
|
rescue => e
|
10
|
-
|
10
|
+
Ahoy.log "Geocode error: #{e.class.name}: #{e.message}"
|
11
11
|
nil
|
12
12
|
end
|
13
13
|
|
14
|
-
if location
|
14
|
+
if location && location.country.present?
|
15
15
|
data = {
|
16
|
-
|
17
|
-
country: location.try(:country).presence,
|
16
|
+
country: location.country,
|
18
17
|
region: location.try(:state).presence,
|
19
18
|
city: location.try(:city).presence,
|
20
19
|
postal_code: location.try(:postal_code).presence,
|
data/lib/ahoy.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
|
+
# stdlib
|
2
|
+
require "ipaddr"
|
3
|
+
|
4
|
+
# dependencies
|
1
5
|
require "active_support"
|
2
6
|
require "active_support/core_ext"
|
3
|
-
require "addressable/uri"
|
4
7
|
require "geocoder"
|
5
8
|
require "safely/core"
|
6
9
|
|
10
|
+
# modules
|
11
|
+
require "ahoy/utils"
|
7
12
|
require "ahoy/base_store"
|
8
13
|
require "ahoy/controller"
|
9
14
|
require "ahoy/database_store"
|
15
|
+
require "ahoy/helper"
|
10
16
|
require "ahoy/model"
|
11
17
|
require "ahoy/query_methods"
|
12
18
|
require "ahoy/tracker"
|
@@ -22,8 +28,15 @@ module Ahoy
|
|
22
28
|
mattr_accessor :visitor_duration
|
23
29
|
self.visitor_duration = 2.years
|
24
30
|
|
31
|
+
mattr_accessor :cookies
|
32
|
+
self.cookies = true
|
33
|
+
|
34
|
+
# TODO deprecate in favor of cookie_options
|
25
35
|
mattr_accessor :cookie_domain
|
26
36
|
|
37
|
+
mattr_accessor :cookie_options
|
38
|
+
self.cookie_options = {}
|
39
|
+
|
27
40
|
mattr_accessor :server_side_visits
|
28
41
|
self.server_side_visits = true
|
29
42
|
|
@@ -56,7 +69,7 @@ module Ahoy
|
|
56
69
|
|
57
70
|
mattr_accessor :user_method
|
58
71
|
self.user_method = lambda do |controller|
|
59
|
-
(controller.respond_to?(:current_user) && controller.current_user) || (controller.respond_to?(:current_resource_owner, true) && controller.send(:current_resource_owner)) || nil
|
72
|
+
(controller.respond_to?(:current_user, true) && controller.send(:current_user)) || (controller.respond_to?(:current_resource_owner, true) && controller.send(:current_resource_owner)) || nil
|
60
73
|
end
|
61
74
|
|
62
75
|
mattr_accessor :exclude_method
|
@@ -64,8 +77,42 @@ module Ahoy
|
|
64
77
|
mattr_accessor :track_bots
|
65
78
|
self.track_bots = false
|
66
79
|
|
80
|
+
mattr_accessor :bot_detection_version
|
81
|
+
self.bot_detection_version = 2
|
82
|
+
|
67
83
|
mattr_accessor :token_generator
|
68
84
|
self.token_generator = -> { SecureRandom.uuid }
|
85
|
+
|
86
|
+
mattr_accessor :mask_ips
|
87
|
+
self.mask_ips = false
|
88
|
+
|
89
|
+
mattr_accessor :user_agent_parser
|
90
|
+
self.user_agent_parser = :device_detector
|
91
|
+
|
92
|
+
mattr_accessor :logger
|
93
|
+
|
94
|
+
def self.log(message)
|
95
|
+
logger.info { "[ahoy] #{message}" } if logger
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.mask_ip(ip)
|
99
|
+
addr = IPAddr.new(ip)
|
100
|
+
if addr.ipv4?
|
101
|
+
# set last octet to 0
|
102
|
+
addr.mask(24).to_s
|
103
|
+
else
|
104
|
+
# set last 80 bits to zeros
|
105
|
+
addr.mask(48).to_s
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.instance
|
110
|
+
Thread.current[:ahoy]
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.instance=(value)
|
114
|
+
Thread.current[:ahoy] = value
|
115
|
+
end
|
69
116
|
end
|
70
117
|
|
71
118
|
ActiveSupport.on_load(:action_controller) do
|
@@ -76,7 +123,15 @@ ActiveSupport.on_load(:active_record) do
|
|
76
123
|
extend Ahoy::Model
|
77
124
|
end
|
78
125
|
|
126
|
+
ActiveSupport.on_load(:action_view) do
|
127
|
+
include Ahoy::Helper
|
128
|
+
end
|
129
|
+
|
79
130
|
# Mongoid
|
131
|
+
# TODO use
|
132
|
+
# ActiveSupport.on_load(:mongoid) do
|
133
|
+
# Mongoid::Document::ClassMethods.include(Ahoy::Model)
|
134
|
+
# end
|
80
135
|
if defined?(ActiveModel)
|
81
136
|
ActiveModel::Callbacks.include(Ahoy::Model)
|
82
137
|
end
|
data/lib/ahoy/base_store.rb
CHANGED
@@ -24,9 +24,13 @@ module Ahoy
|
|
24
24
|
def user
|
25
25
|
@user ||= begin
|
26
26
|
if Ahoy.user_method.respond_to?(:call)
|
27
|
-
Ahoy.user_method.
|
27
|
+
if Ahoy.user_method.arity == 1
|
28
|
+
Ahoy.user_method.call(controller)
|
29
|
+
else
|
30
|
+
Ahoy.user_method.call(controller, request)
|
31
|
+
end
|
28
32
|
else
|
29
|
-
controller.send(Ahoy.user_method)
|
33
|
+
controller.send(Ahoy.user_method) if controller.respond_to?(Ahoy.user_method, true)
|
30
34
|
end
|
31
35
|
end
|
32
36
|
end
|
@@ -39,10 +43,35 @@ module Ahoy
|
|
39
43
|
Ahoy.token_generator.call
|
40
44
|
end
|
41
45
|
|
46
|
+
def visit_or_create
|
47
|
+
visit
|
48
|
+
end
|
49
|
+
|
42
50
|
protected
|
43
51
|
|
44
52
|
def bot?
|
45
|
-
|
53
|
+
unless defined?(@bot)
|
54
|
+
@bot = begin
|
55
|
+
if request
|
56
|
+
if Ahoy.user_agent_parser == :device_detector
|
57
|
+
detector = DeviceDetector.new(request.user_agent)
|
58
|
+
if Ahoy.bot_detection_version == 2
|
59
|
+
detector.bot? || (detector.device_type.nil? && detector.os_name.nil?)
|
60
|
+
else
|
61
|
+
detector.bot?
|
62
|
+
end
|
63
|
+
else
|
64
|
+
# no need to throw friendly error if browser isn't defined
|
65
|
+
# since will error in visit_properties
|
66
|
+
Browser.new(request.user_agent).bot?
|
67
|
+
end
|
68
|
+
else
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
@bot
|
46
75
|
end
|
47
76
|
|
48
77
|
def exclude_by_method?
|
data/lib/ahoy/controller.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "request_store"
|
2
|
-
|
3
1
|
module Ahoy
|
4
2
|
module Controller
|
5
3
|
def self.included(base)
|
@@ -9,7 +7,7 @@ module Ahoy
|
|
9
7
|
end
|
10
8
|
base.before_action :set_ahoy_cookies, unless: -> { Ahoy.api_only }
|
11
9
|
base.before_action :track_ahoy_visit, unless: -> { Ahoy.api_only }
|
12
|
-
base.
|
10
|
+
base.around_action :set_ahoy_request_store
|
13
11
|
end
|
14
12
|
|
15
13
|
def ahoy
|
@@ -21,18 +19,33 @@ module Ahoy
|
|
21
19
|
end
|
22
20
|
|
23
21
|
def set_ahoy_cookies
|
24
|
-
|
25
|
-
|
22
|
+
if Ahoy.cookies
|
23
|
+
ahoy.set_visitor_cookie
|
24
|
+
ahoy.set_visit_cookie
|
25
|
+
else
|
26
|
+
# delete cookies if exist
|
27
|
+
ahoy.reset
|
28
|
+
end
|
26
29
|
end
|
27
30
|
|
28
31
|
def track_ahoy_visit
|
29
|
-
|
30
|
-
|
32
|
+
defer = Ahoy.server_side_visits != true
|
33
|
+
|
34
|
+
if defer && !Ahoy.cookies
|
35
|
+
# avoid calling new_visit?, which triggers a database call
|
36
|
+
elsif ahoy.new_visit?
|
37
|
+
ahoy.track_visit(defer: defer)
|
31
38
|
end
|
32
39
|
end
|
33
40
|
|
34
41
|
def set_ahoy_request_store
|
35
|
-
|
42
|
+
previous_value = Ahoy.instance
|
43
|
+
begin
|
44
|
+
Ahoy.instance = ahoy
|
45
|
+
yield
|
46
|
+
ensure
|
47
|
+
Ahoy.instance = previous_value
|
48
|
+
end
|
36
49
|
end
|
37
50
|
end
|
38
51
|
end
|
data/lib/ahoy/database_store.rb
CHANGED
@@ -4,31 +4,39 @@ module Ahoy
|
|
4
4
|
@visit = visit_model.create!(slice_data(visit_model, data))
|
5
5
|
rescue => e
|
6
6
|
raise e unless unique_exception?(e)
|
7
|
-
|
7
|
+
|
8
|
+
# so next call to visit will try to fetch from DB
|
9
|
+
if defined?(@visit)
|
10
|
+
remove_instance_variable(:@visit)
|
11
|
+
end
|
8
12
|
end
|
9
13
|
|
10
14
|
def track_event(data)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
visit = visit_or_create(started_at: data[:time])
|
16
|
+
if visit
|
17
|
+
event = event_model.new(slice_data(event_model, data))
|
18
|
+
event.visit = visit
|
19
|
+
event.time = visit.started_at if event.time < visit.started_at
|
20
|
+
begin
|
21
|
+
event.save!
|
22
|
+
rescue => e
|
23
|
+
raise e unless unique_exception?(e)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
Ahoy.log "Event excluded since visit not created: #{data[:visit_token]}"
|
20
27
|
end
|
21
28
|
end
|
22
29
|
|
23
30
|
def geocode(data)
|
24
|
-
|
31
|
+
visit_token = data.delete(:visit_token)
|
32
|
+
data = slice_data(visit_model, data)
|
25
33
|
if defined?(Mongoid::Document) && visit_model < Mongoid::Document
|
26
34
|
# upsert since visit might not be found due to eventual consistency
|
27
|
-
visit_model.where(visit_token:
|
35
|
+
visit_model.where(visit_token: visit_token).find_one_and_update({"$set": data}, {upsert: true})
|
28
36
|
elsif visit
|
29
|
-
visit.
|
37
|
+
visit.update!(data)
|
30
38
|
else
|
31
|
-
|
39
|
+
Ahoy.log "Visit for geocode not found: #{visit_token}"
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
@@ -44,7 +52,16 @@ module Ahoy
|
|
44
52
|
end
|
45
53
|
|
46
54
|
def visit
|
47
|
-
@visit
|
55
|
+
unless defined?(@visit)
|
56
|
+
@visit = visit_model.where(visit_token: ahoy.visit_token).first if ahoy.visit_token
|
57
|
+
end
|
58
|
+
@visit
|
59
|
+
end
|
60
|
+
|
61
|
+
# if we don't have a visit, let's try to create one first
|
62
|
+
def visit_or_create(started_at: nil)
|
63
|
+
ahoy.track_visit(started_at: started_at) if !visit && Ahoy.server_side_visits
|
64
|
+
visit
|
48
65
|
end
|
49
66
|
|
50
67
|
protected
|
@@ -59,7 +76,7 @@ module Ahoy
|
|
59
76
|
|
60
77
|
def slice_data(model, data)
|
61
78
|
column_names = model.try(:column_names) || model.attribute_names
|
62
|
-
data.slice(*column_names.map(&:to_sym)).select { |_, v| v }
|
79
|
+
data.slice(*column_names.map(&:to_sym)).select { |_, v| !v.nil? }
|
63
80
|
end
|
64
81
|
|
65
82
|
def unique_exception?(e)
|
data/lib/ahoy/engine.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module Ahoy
|
2
2
|
class Engine < ::Rails::Engine
|
3
|
-
initializer "ahoy", after: "sprockets.environment" do
|
3
|
+
initializer "ahoy", after: "sprockets.environment" do
|
4
|
+
Ahoy.logger ||= Rails.logger
|
5
|
+
|
4
6
|
# allow Devise to be loaded after Ahoy
|
5
7
|
require "ahoy/warden" if defined?(Warden)
|
6
8
|
|
data/lib/ahoy/helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Ahoy
|
2
|
+
module Helper
|
3
|
+
def amp_event(name, properties = {})
|
4
|
+
url = Ahoy::Engine.routes.url_helpers.events_url(
|
5
|
+
url_options.slice(:host, :port, :protocol).merge(
|
6
|
+
name: name,
|
7
|
+
properties: properties,
|
8
|
+
screen_width: "SCREEN_WIDTH",
|
9
|
+
screen_height: "SCREEN_HEIGHT",
|
10
|
+
platform: "Web",
|
11
|
+
landing_page: "AMPDOC_URL",
|
12
|
+
referrer: "DOCUMENT_REFERRER",
|
13
|
+
random: "RANDOM"
|
14
|
+
)
|
15
|
+
)
|
16
|
+
url = "#{url}&visit_token=${clientId(ahoy_visit)}&visitor_token=${clientId(ahoy_visitor)}"
|
17
|
+
|
18
|
+
content_tag "amp-analytics" do
|
19
|
+
content_tag "script", type: "application/json" do
|
20
|
+
json_escape({
|
21
|
+
requests: {
|
22
|
+
pageview: url
|
23
|
+
},
|
24
|
+
triggers: {
|
25
|
+
trackPageview: {
|
26
|
+
on: "visible",
|
27
|
+
request: "pageview"
|
28
|
+
}
|
29
|
+
},
|
30
|
+
transport: {
|
31
|
+
beacon: true,
|
32
|
+
xhrpost: true,
|
33
|
+
image: false
|
34
|
+
}
|
35
|
+
}.to_json).html_safe
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/ahoy/model.rb
CHANGED
@@ -2,12 +2,12 @@ module Ahoy
|
|
2
2
|
module Model
|
3
3
|
def visitable(name = :visit, **options)
|
4
4
|
class_eval do
|
5
|
-
belongs_to(name,
|
5
|
+
belongs_to(name, class_name: "Ahoy::Visit", optional: true, **options)
|
6
6
|
before_create :set_ahoy_visit
|
7
7
|
end
|
8
8
|
class_eval %{
|
9
9
|
def set_ahoy_visit
|
10
|
-
self.#{name} ||=
|
10
|
+
self.#{name} ||= Ahoy.instance.try(:visit_or_create)
|
11
11
|
end
|
12
12
|
}
|
13
13
|
end
|
data/lib/ahoy/query_methods.rb
CHANGED
@@ -27,10 +27,11 @@ module Ahoy
|
|
27
27
|
v = "true"
|
28
28
|
end
|
29
29
|
|
30
|
-
relation = relation.where("JSON_UNQUOTE(properties -> ?) = ?", "$.#{k
|
30
|
+
relation = relation.where("JSON_UNQUOTE(properties -> ?) = ?", "$.#{k}", v.as_json)
|
31
31
|
end
|
32
32
|
else
|
33
33
|
properties.each do |k, v|
|
34
|
+
# TODO cast to json instead
|
34
35
|
relation = relation.where("properties REGEXP ?", "[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]")
|
35
36
|
end
|
36
37
|
end
|
@@ -57,6 +58,7 @@ module Ahoy
|
|
57
58
|
end
|
58
59
|
else
|
59
60
|
properties.each do |k, v|
|
61
|
+
# TODO cast to jsonb instead
|
60
62
|
relation = relation.where("properties SIMILAR TO ?", "%[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]%")
|
61
63
|
end
|
62
64
|
end
|
@@ -66,6 +68,49 @@ module Ahoy
|
|
66
68
|
relation
|
67
69
|
end
|
68
70
|
alias_method :where_properties, :where_props
|
71
|
+
|
72
|
+
def group_prop(*props)
|
73
|
+
# like with group
|
74
|
+
props.flatten!
|
75
|
+
|
76
|
+
relation = self
|
77
|
+
if respond_to?(:columns_hash)
|
78
|
+
column_type = columns_hash["properties"].type
|
79
|
+
adapter_name = connection.adapter_name.downcase
|
80
|
+
else
|
81
|
+
adapter_name = "mongoid"
|
82
|
+
end
|
83
|
+
case adapter_name
|
84
|
+
when "mongoid"
|
85
|
+
raise "Adapter not supported: #{adapter_name}"
|
86
|
+
when /mysql/
|
87
|
+
if connection.try(:mariadb?)
|
88
|
+
props.each do |prop|
|
89
|
+
quoted_prop = connection.quote("$.#{prop}")
|
90
|
+
relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(properties, #{quoted_prop}))")
|
91
|
+
end
|
92
|
+
else
|
93
|
+
column = column_type == :json ? "properties" : "CAST(properties AS JSON)"
|
94
|
+
props.each do |prop|
|
95
|
+
quoted_prop = connection.quote("$.#{prop}")
|
96
|
+
relation = relation.group("JSON_UNQUOTE(JSON_EXTRACT(#{column}, #{quoted_prop}))")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
when /postgres|postgis/
|
100
|
+
# convert to jsonb to fix
|
101
|
+
# could not identify an equality operator for type json
|
102
|
+
# and for text columns
|
103
|
+
cast = [:jsonb, :hstore].include?(column_type) ? "" : "::jsonb"
|
104
|
+
|
105
|
+
props.each do |prop|
|
106
|
+
quoted_prop = connection.quote(prop)
|
107
|
+
relation = relation.group("properties#{cast} -> #{quoted_prop}")
|
108
|
+
end
|
109
|
+
else
|
110
|
+
raise "Adapter not supported: #{adapter_name}"
|
111
|
+
end
|
112
|
+
relation
|
113
|
+
end
|
69
114
|
end
|
70
115
|
end
|
71
116
|
end
|