scopiform 0.2.0 → 0.2.5

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