scopiform 0.2.0 → 0.2.5

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: 934151f9380f5ddc631f49bf933feb2a4763c899a47cae97e17dd682931074c2
4
- data.tar.gz: 7b11d4ffe28fbc13613932b4b3aad56cd01e3ad76a18c3b90286e652ca80b847
3
+ metadata.gz: 5f4f7678ce36e47c795736985a53b7656a2223afbd0b119702a2ccd41ff2d523
4
+ data.tar.gz: 282cfccb56d1760a2ae9d2e55bc4706555684086c0fc906161eefaf663d13865
5
5
  SHA512:
6
- metadata.gz: 28c1c3c6a4dadfa6a3a00271fce88c6471c770e0d8249db754f7720ec2f47af6418bf450fd8bf22a85d995190906cc0f833b310081dd9773f8af33fc17fd6e3d
7
- data.tar.gz: 726a2fa46e53224a825d55a55d4995198a4458e4a3a12ce54d2d55278cc5a5e04ec8c825682bb6d228207d41810d4bf96d93bf773341e784fa24cf6de8fec637
6
+ metadata.gz: 8ea922c85bd6098946058bbafe2cf3b93b7012535af34932eb2168b0569b19b86d25a3fadf97e30265e49132a746dffab93a30889a8e29d6683fc71d03291f04
7
+ data.tar.gz: 68daa6ffcbe5ddc91e61b68a662bc52352a860eb64e31a511473177b0454497e937f947e7cd3300844faf34b47b49904cba0292043d7e8c1c81c6d8db7b08c7c
@@ -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,22 +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
- arel_column = Arel::Nodes::NamedFunction.new(
30
- 'CAST',
31
- [arel_column.as('TEXT')]
32
- )
33
-
29
+ if Helpers::NUMBER_TYPES.include? type
30
+ cast = true
34
31
  type = :string
35
32
  end
36
33
 
37
34
  auto_scope_add(
38
35
  name,
39
- 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
+ },
40
41
  suffix: '_contains',
41
42
  argument_type: type,
42
43
  remove_for_enum: true
@@ -44,7 +45,11 @@ module Scopiform
44
45
 
45
46
  auto_scope_add(
46
47
  name,
47
- 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
+ },
48
53
  suffix: '_not_contains',
49
54
  argument_type: type,
50
55
  remove_for_enum: true
@@ -52,7 +57,11 @@ module Scopiform
52
57
 
53
58
  auto_scope_add(
54
59
  name,
55
- 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
+ },
56
65
  suffix: '_starts_with',
57
66
  argument_type: type,
58
67
  remove_for_enum: true
@@ -60,7 +69,11 @@ module Scopiform
60
69
 
61
70
  auto_scope_add(
62
71
  name,
63
- 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
+ },
64
77
  suffix: '_not_starts_with',
65
78
  argument_type: type,
66
79
  remove_for_enum: true
@@ -68,7 +81,11 @@ module Scopiform
68
81
 
69
82
  auto_scope_add(
70
83
  name,
71
- 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
+ },
72
89
  suffix: '_ends_with',
73
90
  argument_type: type,
74
91
  remove_for_enum: true
@@ -76,7 +93,11 @@ module Scopiform
76
93
 
77
94
  auto_scope_add(
78
95
  name,
79
- 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
+ },
80
101
  suffix: '_not_ends_with',
81
102
  argument_type: type,
82
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.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.0'.freeze
2
+ VERSION = '0.2.5'.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.0
4
+ version: 0.2.5
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-17 00:00:00.000000000 Z
11
+ date: 2021-01-14 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,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