scopiform 0.2.2 → 0.2.3

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 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