scopiform 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3410bc990d8cdf96301ed976fd2a604f19712ec559a6e7a0e2eb8f384f887ba4
4
- data.tar.gz: ff9cf85f92087f0cca007130236776532617002c4dfb9e8f557dc2f96b744d1f
3
+ metadata.gz: bdd67fcd99f42ab665dd31c6fedebe66f490e28ccb03d5d9ad5bd68d6c074b04
4
+ data.tar.gz: eb6622a1ca3489a78156e290020c36153ddd6008e9a97af05804b94b8cf4a2e4
5
5
  SHA512:
6
- metadata.gz: 7000af69a75cad3e5092d27567280207a3301f6dd507b66d1477ee6695aee22de7ff48b9f547644ae4bb597d6289690f885e1f903dd1710c7513dffa4a14512d
7
- data.tar.gz: 1c91fb0c3b91dfd73049fc80f1c09895d987ee125754a912ec0a70c55a8833b5db4c858d102569bb78d7f47dd266b1d65ea6e5caf2c7465297804cf419969bfc
6
+ metadata.gz: ebbbc3f4ed13cddd576958a6b82f5bd2238cb8930ac242ed3dfaeca6bdd71f388c9eaa33912cf02acdfaf31f710737aab3bbcbf2a1654f25a199ad5443eb53d5
7
+ data.tar.gz: 6ea5675e9a5a6d68a2221b4affd5887cd3a0b8a57c407515b693bb65ab0a49b0fde09d7a3c8a39bb1a14d22e074907ce417f92216a82e8b8aa188d269c8c4187
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/concern'
4
+ require 'scopiform/scope_context'
4
5
 
5
6
  module Scopiform
6
7
  module AssociationScopes
@@ -11,12 +12,25 @@ module Scopiform
11
12
  end
12
13
 
13
14
  module ClassMethods
14
- def reflection_added(_name, reflection)
15
- setup_association_auto_scopes(reflection)
15
+ def scopiform_association_scope(association, method, value, ctx:)
16
+ is_root = ctx.nil?
17
+ ctx = ScopeContext.from(ctx)
18
+ ctx.set(arel_table) if is_root || ctx.arel_table.blank?
19
+
20
+ ctx.association = association
21
+ ctx.build_joins
22
+
23
+ applied = ctx.association.klass.send(method, value, ctx: ScopeContext.from(ctx).set(ctx.association_arel_table))
24
+
25
+ if is_root
26
+ joins(ctx.joins).merge(applied)
27
+ else
28
+ all.merge(applied)
29
+ end
16
30
  end
17
31
 
18
- def scopiform_joins(*args, **kargs)
19
- respond_to?(:left_outer_joins) ? left_outer_joins(*args, **kargs) : eager_load(*args, **kargs)
32
+ def reflection_added(_name, reflection)
33
+ setup_association_auto_scopes(reflection)
20
34
  end
21
35
 
22
36
  private
@@ -30,18 +44,8 @@ module Scopiform
30
44
  def setup_association_auto_scopes(association)
31
45
  auto_scope_add(
32
46
  association.name,
33
- proc { |value, joins: nil|
34
- is_root = joins.nil?
35
- joins = {} if is_root
36
-
37
- joins[association.name] ||= {}
38
- applied = association.klass.apply_filters(value, joins: joins[association.name])
39
-
40
- if is_root
41
- scopiform_joins(joins).merge(applied)
42
- else
43
- all.merge(applied)
44
- end
47
+ proc { |value, ctx: nil|
48
+ scopiform_association_scope(association, :apply_filters, value, ctx: ctx)
45
49
  },
46
50
  suffix: '_is',
47
51
  argument_type: :hash
@@ -50,7 +54,9 @@ module Scopiform
50
54
  # Sorting
51
55
  auto_scope_add(
52
56
  association.name,
53
- proc { |value| scopiform_joins(association.name).merge(association.klass.apply_sorts(value)) },
57
+ proc { |value, ctx: nil|
58
+ scopiform_association_scope(association, :apply_sorts, value, ctx: ctx)
59
+ },
54
60
  prefix: 'sort_by_',
55
61
  argument_type: :hash,
56
62
  type: :sort
@@ -21,13 +21,13 @@ module Scopiform
21
21
 
22
22
  auto_scope_add(
23
23
  name,
24
- proc { |value| where(name_sym => value) },
24
+ proc { |value, ctx: nil, **| where(scopiform_arel(ctx)[name_sym].eq(value)) },
25
25
  suffix: '_is',
26
26
  argument_type: type
27
27
  )
28
28
  auto_scope_add(
29
29
  name,
30
- proc { |value| where.not(name_sym => value) },
30
+ proc { |value, ctx: nil, **| where.not(scopiform_arel(ctx)[name_sym].eq(value)) },
31
31
  suffix: '_not',
32
32
  argument_type: type
33
33
  )
@@ -35,7 +35,7 @@ module Scopiform
35
35
  # Sorting
36
36
  auto_scope_add(
37
37
  name,
38
- ->(value = :asc) { order(name_sym => value) },
38
+ proc { |value = :asc, ctx: nil, **| order(scopiform_arel(ctx)[name_sym].send(value.to_s.downcase)) },
39
39
  prefix: 'sort_by_',
40
40
  argument_type: :string,
41
41
  type: :sort
@@ -1,39 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/concern'
4
+ require 'scopiform/scope_context'
4
5
 
5
6
  module Scopiform
6
7
  module Filters
7
8
  extend ActiveSupport::Concern
8
9
 
9
10
  module ClassMethods
10
- def apply_filters(filters_hash, injecting: all, joins: nil)
11
- filters_hash.keys.inject(injecting) { |out, filter_name| resolve_filter(out, filter_name, filters_hash[filter_name], joins) }
11
+ def apply_filters(filters_hash, injecting: all, ctx: nil)
12
+ filters_hash.keys.inject(injecting) { |out, filter_name| resolve_filter(out, filter_name, filters_hash[filter_name], ctx: ctx) }
12
13
  end
13
14
 
14
- def apply_sorts(sorts_hash, injecting = all)
15
- sorts_hash.keys.inject(injecting) { |out, sort_name| resolve_sort(out, sort_name, sorts_hash[sort_name]) }
15
+ def apply_sorts(sorts_hash, injecting = all, ctx: nil)
16
+ sorts_hash.keys.inject(injecting) { |out, sort_name| resolve_sort(out, sort_name, sorts_hash[sort_name], ctx: ctx) }
16
17
  end
17
18
 
18
19
  private
19
20
 
20
- def resolve_filter(out, filter_name, filter_argument, joins)
21
+ def resolve_filter(out, filter_name, filter_argument, ctx:)
21
22
  if filter_name.to_s.casecmp('OR').zero?
22
- or_joins = {}
23
+ ctx ||= ScopeContext.new
24
+ ctx.joins = []
23
25
 
24
26
  return (
25
27
  filter_argument
26
- .map { |or_filters_hash| apply_filters(or_filters_hash, injecting: out, joins: or_joins) }
27
- .map { |a| a.scopiform_joins(or_joins) }
28
+ .map { |or_filters_hash| apply_filters(or_filters_hash, injecting: out, ctx: ctx) }
29
+ .map { |a| a.joins(ctx.joins) }
28
30
  .inject { |chain, applied| chain.or(applied) }
29
31
  )
30
32
  end
31
- out.send(filter_name, filter_argument, joins: joins)
33
+ out.send(filter_name, filter_argument, ctx: ctx)
32
34
  end
33
35
 
34
- def resolve_sort(out, sort_name, sort_argument)
36
+ def resolve_sort(out, sort_name, sort_argument, ctx:)
35
37
  method_name = "sort_by_#{sort_name}"
36
- out.send(method_name, sort_argument)
38
+ out.send(method_name, sort_argument, ctx: ctx)
37
39
  end
38
40
  end
39
41
  end
@@ -55,6 +55,10 @@ module Scopiform
55
55
  defined_enums.include? name.to_s
56
56
  end
57
57
 
58
+ def scopiform_arel(ctx)
59
+ ctx&.arel_table || arel_table
60
+ end
61
+
58
62
  protected
59
63
 
60
64
  def safe_columns
@@ -0,0 +1,74 @@
1
+ module Scopiform
2
+ class ScopeContext
3
+ attr_accessor :association, :arel_table, :association_arel_table, :joins, :ancestors
4
+
5
+ def self.from(ctx)
6
+ created = new
7
+
8
+ if ctx
9
+ created.set(ctx.arel_table)
10
+ created.association = ctx.association
11
+ created.association_arel_table = ctx.association_arel_table
12
+ created.joins = ctx.joins
13
+ created.ancestors = [*ctx.ancestors]
14
+ end
15
+
16
+ created
17
+ end
18
+
19
+ def initialize
20
+ @joins = []
21
+ @ancestors = []
22
+ end
23
+
24
+ def set(arel_table)
25
+ @arel_table = arel_table
26
+
27
+ self
28
+ end
29
+
30
+ def build_joins
31
+ loop do
32
+ if association.through_reflection
33
+ source_reflection_name = association.source_reflection_name
34
+ self.association = association.through_reflection
35
+ end
36
+
37
+ ancestors << association.name
38
+ self.association_arel_table = association.klass.arel_table.alias(alias_name)
39
+
40
+ joins << create_join
41
+
42
+ break if source_reflection_name.blank?
43
+
44
+ self.association = association.klass.association(source_reflection_name)
45
+ self.arel_table = association_arel_table
46
+ end
47
+
48
+ self
49
+ end
50
+
51
+ def alias_name
52
+ ancestors.join('_').downcase
53
+ end
54
+
55
+ private
56
+
57
+ def conditions
58
+ # Rails 4 join_keys has arity of 1, expecting a klass as an argument
59
+ keys = association.method(:join_keys).arity == 1 ? association.join_keys(association.klass) : association.join_keys
60
+ [*keys.foreign_key]
61
+ .zip([*keys.key])
62
+ .map { |foreign, primary| arel_table[foreign].eq(association_arel_table[primary]) }
63
+ .reduce { |acc, cond| acc.and(cond) }
64
+ end
65
+
66
+ def create_join
67
+ arel_table.create_join(
68
+ association_arel_table,
69
+ association_arel_table.create_on(conditions),
70
+ Arel::Nodes::OuterJoin
71
+ )
72
+ end
73
+ end
74
+ end
@@ -20,46 +20,45 @@ module Scopiform
20
20
  name = column.name
21
21
  name_sym = name.to_sym
22
22
  type = column.type
23
- arel_column = arel_table[name]
24
23
 
25
24
  auto_scope_add(
26
25
  name,
27
- proc { |value| where(name_sym => value) },
26
+ proc { |*value, ctx: nil, **| where(scopiform_arel(ctx)[name_sym].in(value.flatten)) },
28
27
  suffix: '_in',
29
28
  argument_type: [type]
30
29
  )
31
30
 
32
31
  auto_scope_add(
33
32
  name,
34
- proc { |value| where.not(name_sym => value) },
33
+ proc { |*value, ctx: nil, **| where.not(scopiform_arel(ctx)[name_sym].in(value.flatten)) },
35
34
  suffix: '_not_in',
36
35
  argument_type: [type]
37
36
  )
38
37
 
39
38
  auto_scope_add(
40
39
  name,
41
- proc { |value| where(arel_column.lt(value)) },
40
+ proc { |value, ctx: nil, **| where(scopiform_arel(ctx)[name_sym].lt(value)) },
42
41
  suffix: '_lt',
43
42
  argument_type: type
44
43
  )
45
44
 
46
45
  auto_scope_add(
47
46
  name,
48
- proc { |value| where(arel_column.lteq(value)) },
47
+ proc { |value, ctx: nil, **| where(scopiform_arel(ctx)[name_sym].lteq(value)) },
49
48
  suffix: '_lte',
50
49
  argument_type: type
51
50
  )
52
51
 
53
52
  auto_scope_add(
54
53
  name,
55
- proc { |value| where(arel_column.gt(value)) },
54
+ proc { |value, ctx: nil, **| where(scopiform_arel(ctx)[name_sym].gt(value)) },
56
55
  suffix: '_gt',
57
56
  argument_type: type
58
57
  )
59
58
 
60
59
  auto_scope_add(
61
60
  name,
62
- proc { |value| where(arel_column.gteq(value)) },
61
+ proc { |value, ctx: nil, **| where(scopiform_arel(ctx)[name_sym].gteq(value)) },
63
62
  suffix: '_gte',
64
63
  argument_type: type
65
64
  )
@@ -13,6 +13,14 @@ module Scopiform
13
13
  module ClassMethods
14
14
  private
15
15
 
16
+ def cast_to_text(arel_column)
17
+ cast_to = connection.adapter_name.downcase.starts_with?('mysql') ? 'CHAR' : 'TEXT'
18
+ Arel::Nodes::NamedFunction.new(
19
+ 'CAST',
20
+ [arel_column.as(cast_to)]
21
+ )
22
+ end
23
+
16
24
  def setup_string_and_number_auto_scopes
17
25
  string_numbers = Helpers::STRING_TYPES + Helpers::NUMBER_TYPES
18
26
  string_number_columns = safe_columns.select do |column|
@@ -21,25 +29,22 @@ module Scopiform
21
29
  string_number_columns.each do |column|
22
30
  name = column.name
23
31
  type = column.type
24
- arel_column = arel_table[name]
32
+ cast = false
25
33
 
26
34
  # Numeric values don't work properly with `.matches`. Using workaround
27
35
  # https://coderwall.com/p/qtgvdq/using-arel_table-for-ilike-yes-even-with-integers
28
- if Helpers::NUMBER_TYPES.include? column.type
29
- cast_to = 'TEXT'
30
- cast_to = 'CHAR' if connection.adapter_name.downcase.starts_with? 'mysql'
31
-
32
- arel_column = Arel::Nodes::NamedFunction.new(
33
- 'CAST',
34
- [arel_column.as(cast_to)]
35
- )
36
-
36
+ if Helpers::NUMBER_TYPES.include? type
37
+ cast = true
37
38
  type = :string
38
39
  end
39
40
 
40
41
  auto_scope_add(
41
42
  name,
42
- proc { |value| where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
43
+ proc { |value, ctx: nil, **|
44
+ arel_column = scopiform_arel(ctx)[name]
45
+ arel_column = cast_to_text(arel_column) if cast
46
+ where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"))
47
+ },
43
48
  suffix: '_contains',
44
49
  argument_type: type,
45
50
  remove_for_enum: true
@@ -47,7 +52,11 @@ module Scopiform
47
52
 
48
53
  auto_scope_add(
49
54
  name,
50
- proc { |value| where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
55
+ proc { |value, ctx: nil, **|
56
+ arel_column = scopiform_arel(ctx)[name]
57
+ arel_column = cast_to_text(arel_column) if cast
58
+ where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"))
59
+ },
51
60
  suffix: '_not_contains',
52
61
  argument_type: type,
53
62
  remove_for_enum: true
@@ -55,7 +64,11 @@ module Scopiform
55
64
 
56
65
  auto_scope_add(
57
66
  name,
58
- proc { |value| where(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
67
+ proc { |value, ctx: nil, **|
68
+ arel_column = scopiform_arel(ctx)[name]
69
+ arel_column = cast_to_text(arel_column) if cast
70
+ where(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"))
71
+ },
59
72
  suffix: '_starts_with',
60
73
  argument_type: type,
61
74
  remove_for_enum: true
@@ -63,7 +76,11 @@ module Scopiform
63
76
 
64
77
  auto_scope_add(
65
78
  name,
66
- proc { |value| where.not(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
79
+ proc { |value, ctx: nil, **|
80
+ arel_column = scopiform_arel(ctx)[name]
81
+ arel_column = cast_to_text(arel_column) if cast
82
+ where.not(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"))
83
+ },
67
84
  suffix: '_not_starts_with',
68
85
  argument_type: type,
69
86
  remove_for_enum: true
@@ -71,7 +88,11 @@ module Scopiform
71
88
 
72
89
  auto_scope_add(
73
90
  name,
74
- proc { |value| where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}")) },
91
+ proc { |value, ctx: nil, **|
92
+ arel_column = scopiform_arel(ctx)[name]
93
+ arel_column = cast_to_text(arel_column) if cast
94
+ where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}"))
95
+ },
75
96
  suffix: '_ends_with',
76
97
  argument_type: type,
77
98
  remove_for_enum: true
@@ -79,7 +100,11 @@ module Scopiform
79
100
 
80
101
  auto_scope_add(
81
102
  name,
82
- proc { |value| where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}")) },
103
+ proc { |value, ctx: nil, **|
104
+ arel_column = scopiform_arel(ctx)[name]
105
+ arel_column = cast_to_text(arel_column) if cast
106
+ where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}"))
107
+ },
83
108
  suffix: '_not_ends_with',
84
109
  argument_type: type,
85
110
  remove_for_enum: true
@@ -1,3 +1,3 @@
1
1
  module Scopiform
2
- VERSION = '0.2.2'.freeze
2
+ VERSION = '0.2.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scopiform
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - jayce.pulsipher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-19 00:00:00.000000000 Z
11
+ date: 2020-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: composite_primary_keys
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  description:
70
84
  email:
71
85
  - jayce.pulsipher@3-form.com
@@ -83,6 +97,7 @@ files:
83
97
  - lib/scopiform/filters.rb
84
98
  - lib/scopiform/helpers.rb
85
99
  - lib/scopiform/reflection_plugin.rb
100
+ - lib/scopiform/scope_context.rb
86
101
  - lib/scopiform/scope_definition.rb
87
102
  - lib/scopiform/string_number_date_scopes.rb
88
103
  - lib/scopiform/string_number_scopes.rb