scopiform 0.2.1 → 0.2.6

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: ecc39ac0b6ce6d7e5c685fcd9baf94aff3595641362d8ea78cf7b10553d2373b
4
- data.tar.gz: d5cca316fc383e63d2dbc1d76f67eb2aa405c4dee19f973860a7553d8eefca8c
3
+ metadata.gz: ae35b0a41cb186989dbb8f76fb8dbfa11604782e6b795fca0ac5e50e66bdd3fa
4
+ data.tar.gz: 88b9bb3f9cf86b143d997502d0ae997606652d813541259c684d76c7c379b40d
5
5
  SHA512:
6
- metadata.gz: beb86577a3d13cbddf15ad2a90bb93d3422c8a3243c09462153977fd95a917d4fdbb3e011466dca362be54a9b3c3a6687341a9215a6fda43e4a7d801a2ae5f37
7
- data.tar.gz: 4510cbd741943ccf3fe7313502997a3ec0430a974569d98e79369e3e0cd5901520ef1cbfada7d708acaef133feb85f09b40a1194e273318e7681ee8563c0164a
6
+ metadata.gz: a6239fb1c253b895b3bf35f640fde0f600cc853f9348839108536abf2eed887c71c763b1c1ebb71fa4d2eff07bb77b953732641edb4dde3f271127d25d76287e
7
+ data.tar.gz: fde9049ba6c66da3901eb99352b3e938130a46e8a24e85c42a04a92c4c641188e112299ea2882b632fe3f6d833f300d9bc8f7aa52928a69b4df7653377af268f
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/concern'
4
+ require 'scopiform/utilities'
4
5
 
5
6
  module Scopiform
6
7
  module AssociationScopes
@@ -15,10 +16,6 @@ module Scopiform
15
16
  setup_association_auto_scopes(reflection)
16
17
  end
17
18
 
18
- def scopiform_joins(*args, **kargs)
19
- respond_to?(:left_outer_joins) ? left_outer_joins(*args, **kargs) : eager_load(*args, **kargs)
20
- end
21
-
22
19
  private
23
20
 
24
21
  def setup_associations_auto_scopes
@@ -30,18 +27,8 @@ module Scopiform
30
27
  def setup_association_auto_scopes(association)
31
28
  auto_scope_add(
32
29
  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
30
+ proc { |value, ctx: nil|
31
+ Utilities.association_scope(self, association, :apply_filters, value, ctx: ctx)
45
32
  },
46
33
  suffix: '_is',
47
34
  argument_type: :hash
@@ -50,7 +37,9 @@ module Scopiform
50
37
  # Sorting
51
38
  auto_scope_add(
52
39
  association.name,
53
- proc { |value| scopiform_joins(association.name).merge(association.klass.apply_sorts(value)) },
40
+ proc { |value, ctx: nil|
41
+ Utilities.association_scope(self, association, :apply_sorts, value, ctx: ctx)
42
+ },
54
43
  prefix: 'sort_by_',
55
44
  argument_type: :hash,
56
45
  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
@@ -8,6 +8,8 @@ module Scopiform
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  module ClassMethods
11
+ attr_accessor :scopiform_ctx
12
+
11
13
  def auto_scopes
12
14
  @auto_scopes || []
13
15
  end
@@ -18,7 +20,7 @@ module Scopiform
18
20
  end
19
21
 
20
22
  def auto_scope?(name)
21
- auto_scopes.find { |scope| scope.name == name }
23
+ auto_scopes.find { |scope| scope.name == name }.present?
22
24
  end
23
25
 
24
26
  def auto_scope_add(attribute, block, prefix: nil, suffix: nil, **options)
@@ -58,7 +60,7 @@ module Scopiform
58
60
  end
59
61
 
60
62
  if scope.options[:remove_for_enum]
61
- singleton_class.remove_method scope.name
63
+ singleton_class.send(:remove_method, scope.name)
62
64
  @auto_scopes.delete(scope)
63
65
  end
64
66
  end
@@ -74,7 +76,11 @@ module Scopiform
74
76
 
75
77
  def auto_scope_for_alias(alias_name, scope_definition)
76
78
  alias_scope_definition = scope_definition.dup
77
- alias_scope_definition.attribute = alias_name
79
+ alias_scope_definition.attribute = alias_name.to_sym
80
+
81
+ @auto_scopes ||= []
82
+ @auto_scopes << alias_scope_definition
83
+
78
84
  singleton_class.send(:alias_method, alias_scope_definition.name, scope_definition.name)
79
85
  end
80
86
  end
@@ -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
@@ -43,7 +43,7 @@ module Scopiform
43
43
  def association(name)
44
44
  name = resolve_alias(name)
45
45
  association = reflect_on_association(name)
46
-
46
+
47
47
  association.klass if association.present?
48
48
  association
49
49
  rescue NameError
@@ -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,81 @@
1
+ module Scopiform
2
+ class ScopeContext
3
+ attr_accessor :association, :arel_table, :association_arel_table, :joins, :ancestors, :scopes
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
+ @scopes = []
23
+ end
24
+
25
+ def set(arel_table)
26
+ @arel_table = arel_table
27
+
28
+ self
29
+ end
30
+
31
+ def build_joins
32
+ loop do
33
+ if association.through_reflection
34
+ source_reflection_name = association.source_reflection_name
35
+ self.association = association.through_reflection
36
+ end
37
+
38
+ ancestors << association.name.to_s.pluralize
39
+ self.association_arel_table = association.klass.arel_table.alias(alias_name)
40
+
41
+ joins << create_join
42
+
43
+ if association.scope.present? && association.scope.arity.zero?
44
+ association.klass.scopiform_ctx = ScopeContext.from(self).set(association_arel_table)
45
+ scopes << association.klass.instance_exec(&association.scope)
46
+ association.klass.scopiform_ctx = nil
47
+ end
48
+
49
+ break if source_reflection_name.blank?
50
+
51
+ self.association = association.klass.reflect_on_association(source_reflection_name)
52
+ self.arel_table = association_arel_table
53
+ end
54
+
55
+ joins
56
+ end
57
+
58
+ def alias_name
59
+ ancestors.join('_').downcase
60
+ end
61
+
62
+ private
63
+
64
+ def conditions
65
+ # Rails 4 join_keys has arity of 1, expecting a klass as an argument
66
+ keys = association.method(:join_keys).arity == 1 ? association.join_keys(association.klass) : association.join_keys
67
+ [*keys.foreign_key]
68
+ .zip([*keys.key])
69
+ .map { |foreign, primary| arel_table[foreign].eq(association_arel_table[primary]) }
70
+ .reduce { |acc, cond| acc.and(cond) }
71
+ end
72
+
73
+ def create_join
74
+ arel_table.create_join(
75
+ association_arel_table,
76
+ association_arel_table.create_on(conditions),
77
+ Arel::Nodes::OuterJoin
78
+ )
79
+ end
80
+ end
81
+ end
@@ -14,7 +14,7 @@ module Scopiform
14
14
  end
15
15
 
16
16
  def name_for(attribute_name)
17
- "#{prefix}#{attribute}#{suffix}".underscore.to_sym
17
+ "#{prefix}#{attribute_name}#{suffix}".underscore.to_sym
18
18
  end
19
19
 
20
20
  def dup
@@ -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
  )
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support/concern'
4
+ require 'scopiform/utilities'
4
5
 
5
6
  module Scopiform
6
7
  module StringNumberScopes
@@ -21,25 +22,22 @@ module Scopiform
21
22
  string_number_columns.each do |column|
22
23
  name = column.name
23
24
  type = column.type
24
- arel_column = arel_table[name]
25
+ cast = false
25
26
 
26
27
  # Numeric values don't work properly with `.matches`. Using workaround
27
28
  # 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
-
29
+ if Helpers::NUMBER_TYPES.include? type
30
+ cast = true
37
31
  type = :string
38
32
  end
39
33
 
40
34
  auto_scope_add(
41
35
  name,
42
- proc { |value| where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
36
+ proc { |value, ctx: nil, **|
37
+ arel_column = scopiform_arel(ctx)[name]
38
+ arel_column = Utilities.cast_to_text(arel_column, connection) if cast
39
+ where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"))
40
+ },
43
41
  suffix: '_contains',
44
42
  argument_type: type,
45
43
  remove_for_enum: true
@@ -47,7 +45,11 @@ module Scopiform
47
45
 
48
46
  auto_scope_add(
49
47
  name,
50
- proc { |value| where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
48
+ proc { |value, ctx: nil, **|
49
+ arel_column = scopiform_arel(ctx)[name]
50
+ arel_column = Utilities.cast_to_text(arel_column, connection) if cast
51
+ where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"))
52
+ },
51
53
  suffix: '_not_contains',
52
54
  argument_type: type,
53
55
  remove_for_enum: true
@@ -55,7 +57,11 @@ module Scopiform
55
57
 
56
58
  auto_scope_add(
57
59
  name,
58
- proc { |value| where(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
60
+ proc { |value, ctx: nil, **|
61
+ arel_column = scopiform_arel(ctx)[name]
62
+ arel_column = Utilities.cast_to_text(arel_column, connection) if cast
63
+ where(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"))
64
+ },
59
65
  suffix: '_starts_with',
60
66
  argument_type: type,
61
67
  remove_for_enum: true
@@ -63,7 +69,11 @@ module Scopiform
63
69
 
64
70
  auto_scope_add(
65
71
  name,
66
- proc { |value| where.not(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%")) },
72
+ proc { |value, ctx: nil, **|
73
+ arel_column = scopiform_arel(ctx)[name]
74
+ arel_column = Utilities.cast_to_text(arel_column, connection) if cast
75
+ where.not(arel_column.matches("#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"))
76
+ },
67
77
  suffix: '_not_starts_with',
68
78
  argument_type: type,
69
79
  remove_for_enum: true
@@ -71,7 +81,11 @@ module Scopiform
71
81
 
72
82
  auto_scope_add(
73
83
  name,
74
- proc { |value| where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}")) },
84
+ proc { |value, ctx: nil, **|
85
+ arel_column = scopiform_arel(ctx)[name]
86
+ arel_column = Utilities.cast_to_text(arel_column, connection) if cast
87
+ where(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}"))
88
+ },
75
89
  suffix: '_ends_with',
76
90
  argument_type: type,
77
91
  remove_for_enum: true
@@ -79,7 +93,11 @@ module Scopiform
79
93
 
80
94
  auto_scope_add(
81
95
  name,
82
- proc { |value| where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}")) },
96
+ proc { |value, ctx: nil, **|
97
+ arel_column = scopiform_arel(ctx)[name]
98
+ arel_column = Utilities.cast_to_text(arel_column, connection) if cast
99
+ where.not(arel_column.matches("%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}"))
100
+ },
83
101
  suffix: '_not_ends_with',
84
102
  argument_type: type,
85
103
  remove_for_enum: true
@@ -0,0 +1,30 @@
1
+ require 'scopiform/scope_context'
2
+
3
+ module Scopiform
4
+ module Utilities
5
+ def self.cast_to_text(arel_column, connection)
6
+ cast_to = connection.adapter_name.downcase.starts_with?('mysql') ? 'CHAR' : 'TEXT'
7
+ Arel::Nodes::NamedFunction.new(
8
+ 'CAST',
9
+ [arel_column.as(cast_to)]
10
+ )
11
+ end
12
+
13
+ def self.association_scope(active_record, association, method, value, ctx:)
14
+ is_root = ctx.nil?
15
+ ctx = ScopeContext.from(ctx)
16
+ ctx.set(active_record.arel_table) if is_root || ctx.arel_table.blank?
17
+
18
+ ctx.association = association
19
+ ctx.build_joins
20
+
21
+ applied = ctx.association.klass.send(method, value, ctx: ScopeContext.from(ctx).set(ctx.association_arel_table))
22
+
23
+ if is_root
24
+ ctx.scopes.reduce(active_record.distinct.joins(ctx.joins).merge(applied)) { |chain, scope| chain.merge(scope) }
25
+ else
26
+ ctx.scopes.reduce(active_record.all.merge(applied)) { |chain, scope| chain.merge(scope) }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module Scopiform
2
- VERSION = '0.2.1'.freeze
2
+ VERSION = '0.2.6'.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.1
4
+ version: 0.2.6
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: 2021-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: composite_primary_keys
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: sqlite3
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -83,9 +97,11 @@ 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
104
+ - lib/scopiform/utilities.rb
89
105
  - lib/scopiform/version.rb
90
106
  - lib/tasks/scopiform_tasks.rake
91
107
  homepage: https://github.com/3-form/scopiform
@@ -107,7 +123,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
123
  - !ruby/object:Gem::Version
108
124
  version: '0'
109
125
  requirements: []
110
- rubygems_version: 3.0.3
126
+ rubyforge_project:
127
+ rubygems_version: 2.7.7
111
128
  signing_key:
112
129
  specification_version: 4
113
130
  summary: Generate scope methods to ActiveRecord classes based on columns and associations