prato 0.1.0

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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +938 -0
  5. data/lib/prato/configuration.rb +99 -0
  6. data/lib/prato/internal/active_record_version.rb +24 -0
  7. data/lib/prato/internal/join_helper.rb +48 -0
  8. data/lib/prato/internal/join_helper_legacy.rb +171 -0
  9. data/lib/prato/internal/lazy_loader_cache.rb +25 -0
  10. data/lib/prato/internal/pipeline/filtering.rb +277 -0
  11. data/lib/prato/internal/pipeline/pagination.rb +30 -0
  12. data/lib/prato/internal/pipeline/serializer.rb +87 -0
  13. data/lib/prato/internal/pipeline/sorting.rb +78 -0
  14. data/lib/prato/internal/query_executor.rb +105 -0
  15. data/lib/prato/internal/query_state.rb +90 -0
  16. data/lib/prato/internal/specification.rb +101 -0
  17. data/lib/prato/internal/specification_builder.rb +361 -0
  18. data/lib/prato/internal/sql_support.rb +118 -0
  19. data/lib/prato/query/and_filter.rb +13 -0
  20. data/lib/prato/query/default_parser.rb +148 -0
  21. data/lib/prato/query/field_resolver.rb +23 -0
  22. data/lib/prato/query/filter.rb +15 -0
  23. data/lib/prato/query/or_filter.rb +13 -0
  24. data/lib/prato/query/parameters.rb +17 -0
  25. data/lib/prato/query/sort.rb +14 -0
  26. data/lib/prato/table.rb +39 -0
  27. data/lib/prato/table_builder.rb +40 -0
  28. data/lib/prato/types/aggregate_column.rb +93 -0
  29. data/lib/prato/types/association_column.rb +37 -0
  30. data/lib/prato/types/direct_column.rb +27 -0
  31. data/lib/prato/types/expression_column.rb +38 -0
  32. data/lib/prato/types/ruby_column.rb +31 -0
  33. data/lib/prato/version.rb +5 -0
  34. data/lib/prato.rb +66 -0
  35. metadata +96 -0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Query
5
+ class Parameters
6
+ attr_reader :page, :per_page, :filters, :sorts, :fields
7
+
8
+ def initialize(page: nil, per_page: nil, filters: nil, sorts: nil, fields: nil)
9
+ @page = page
10
+ @per_page = per_page
11
+ @filters = filters
12
+ @sorts = sorts
13
+ @fields = fields
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Query
5
+ class Sort
6
+ attr_reader :field, :is_desc
7
+
8
+ def initialize(field, is_desc)
9
+ @field = field
10
+ @is_desc = is_desc
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ class Table
5
+ def initialize(spec)
6
+ @spec = spec
7
+ end
8
+
9
+ def page(scope, params = nil)
10
+ Internal::QueryExecutor.execute(
11
+ scope,
12
+ @spec,
13
+ raw_params: params,
14
+ paginated: true
15
+ )
16
+ end
17
+
18
+ def full(scope, params = nil)
19
+ Internal::QueryExecutor.execute(
20
+ scope,
21
+ @spec,
22
+ raw_params: params,
23
+ paginated: false
24
+ )
25
+ end
26
+
27
+ def batches(scope, params = nil, batch_size: 1000, &block)
28
+ return enum_for(:batches, scope, params, batch_size: batch_size) unless block
29
+
30
+ Internal::QueryExecutor.execute_in_batches(
31
+ scope,
32
+ @spec,
33
+ raw_params: params,
34
+ batch_size: batch_size,
35
+ &block
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ class TableBuilder
5
+
6
+ attr_reader :spec_builder
7
+
8
+ def initialize
9
+ @spec_builder = Internal::SpecificationBuilder.new
10
+ end
11
+
12
+ def column(*args, **kwargs)
13
+ @spec_builder.inner_column(*args, **kwargs)
14
+ end
15
+
16
+ def ruby_column(*args, **kwargs, &block)
17
+ @spec_builder.inner_ruby_column(*args, **kwargs, &block)
18
+ end
19
+
20
+ def query_column(*args, **kwargs)
21
+ @spec_builder.inner_query_column(*args, **kwargs)
22
+ end
23
+
24
+ def section(id, &block)
25
+ raise ArgumentError, "Section requires a block" unless block_given?
26
+ raise ArgumentError, "Section block must not accept arguments" unless block.parameters.empty?
27
+
28
+ @spec_builder.inner_section(id, &block)
29
+ end
30
+
31
+ def ruby_loader(id, **kwargs, &block)
32
+ @spec_builder.inner_ruby_loader(id, **kwargs, &block)
33
+ end
34
+
35
+ def configure(config = nil, **overrides)
36
+ resolved_config = Configuration.with_settings(config, **overrides)
37
+ @spec_builder.inner_config(resolved_config)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Types
5
+ class AggregateColumn
6
+ attr_reader :arel_node, :format, :filter
7
+
8
+ def initialize(aggregate_function, aggregate_accessor, format: nil, filter: nil)
9
+ @accessor = Array(aggregate_accessor)
10
+ @aggregate_function = aggregate_function
11
+ @format = format
12
+ @filter = filter
13
+ end
14
+
15
+ def resolve_arel!(base_model, display_id)
16
+ association_path = @aggregate_function == :count ? @accessor : @accessor[0..-2]
17
+ aggregate_field = @aggregate_function == :count ? nil : @accessor[-1]
18
+
19
+ reflections = resolve_reflections(base_model, association_path)
20
+ base_table = base_model.arel_table
21
+ aliased_tables = reflections.each_with_index.map do |reflection, index|
22
+ Arel::Table.new(reflection.klass.table_name, as: "prato_agg_#{index}_#{reflection.klass.table_name}")
23
+ end
24
+ target_table = aliased_tables.last
25
+
26
+ subquery = target_table.project(aggregate_expression(target_table, @aggregate_function, aggregate_field))
27
+
28
+ (reflections.length - 1).downto(1) do |i|
29
+ ref = reflections[i]
30
+ source_table = aliased_tables[i - 1]
31
+ subquery = subquery.join(source_table).on(
32
+ association_condition(ref, source_table, aliased_tables[i])
33
+ )
34
+ end
35
+
36
+ first_ref = reflections.first
37
+ subquery = subquery.where(
38
+ association_condition(first_ref, base_table, aliased_tables.first)
39
+ )
40
+
41
+ @arel_node = Arel::Nodes::Grouping.new(subquery)
42
+ @sql_alias = display_id.to_s
43
+ end
44
+
45
+ def sql_node_for(_scope)
46
+ @arel_node
47
+ end
48
+
49
+ def select_node
50
+ Arel::Nodes::As.new(@arel_node, Arel.sql(@sql_alias))
51
+ end
52
+
53
+ def extract_value(record, _)
54
+ record[@sql_alias]
55
+ end
56
+
57
+ private
58
+
59
+ def resolve_reflections(base_model, path)
60
+ current_model = base_model
61
+ path.map do |assoc_name|
62
+ reflection = current_model.reflect_on_association(assoc_name)
63
+ raise ArgumentError, "Unknown association '#{assoc_name}' on #{current_model}" unless reflection
64
+
65
+ current_model = reflection.klass
66
+ reflection
67
+ end
68
+ end
69
+
70
+ def association_condition(reflection, source_table, target_table)
71
+ if reflection.macro == :belongs_to
72
+ source_table[reflection.foreign_key].eq(
73
+ target_table[reflection.active_record_primary_key]
74
+ )
75
+ else
76
+ target_table[reflection.foreign_key].eq(
77
+ source_table[reflection.active_record_primary_key]
78
+ )
79
+ end
80
+ end
81
+
82
+ def aggregate_expression(table, aggregate_function, aggregate_field)
83
+ case aggregate_function
84
+ when :count then Arel.star.count
85
+ when :sum then Arel::Nodes::NamedFunction.new("COALESCE", [table[aggregate_field].sum, 0])
86
+ when :avg then table[aggregate_field].average
87
+ when :min then table[aggregate_field].minimum
88
+ when :max then table[aggregate_field].maximum
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Types
5
+ class AssociationColumn
6
+ attr_reader :association_path, :format, :filter
7
+
8
+ def initialize(accessor, format: nil, filter: nil)
9
+ @association_path = accessor[0..-2].map(&:to_sym).freeze
10
+ @attribute_name = accessor[-1].to_sym
11
+ @format = format
12
+ @filter = filter
13
+ end
14
+
15
+ def resolve_arel!(base_model, _display_id)
16
+ current_model = base_model
17
+
18
+ @association_path.each do |assoc_name|
19
+ reflection = current_model.reflect_on_association(assoc_name)
20
+ raise ArgumentError, "Unknown association '#{assoc_name}' on #{current_model}" unless reflection
21
+
22
+ current_model = reflection.klass
23
+ end
24
+ end
25
+
26
+ def sql_node_for(scope)
27
+ table = Internal::SqlSupport.table_for(scope, @association_path)
28
+ table[@attribute_name]
29
+ end
30
+
31
+ def extract_value(record, _ruby_data)
32
+ target = @association_path.reduce(record) { |obj, assoc| obj&.public_send(assoc) }
33
+ target&.[](@attribute_name)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Types
5
+ class DirectColumn
6
+ attr_reader :format, :filter
7
+
8
+ def initialize(accessor, format: nil, filter: nil)
9
+ @attribute_name = accessor.is_a?(Array) ? accessor.first : accessor
10
+ @format = format
11
+ @filter = filter
12
+ end
13
+
14
+ def resolve_arel!(base_model, _display_id)
15
+ @arel_node = base_model.arel_table[@attribute_name]
16
+ end
17
+
18
+ def sql_node_for(_scope)
19
+ @arel_node
20
+ end
21
+
22
+ def extract_value(record, _ruby_data)
23
+ record[@attribute_name]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Types
5
+ class ExpressionColumn
6
+ attr_reader :arel_node, :format, :filter
7
+
8
+ def initialize(expression, format: nil, filter: nil)
9
+ @expression = expression
10
+ @format = format
11
+ @filter = filter
12
+ end
13
+
14
+ def resolve_arel!(base_model, display_id)
15
+ expression_sql = if @expression.is_a?(Symbol)
16
+ base_model.public_send(@expression)
17
+ else
18
+ @expression
19
+ end
20
+
21
+ @sql_alias = display_id.to_s
22
+ @arel_node = Arel::Nodes::Grouping.new(Arel.sql(expression_sql))
23
+ end
24
+
25
+ def sql_node_for(_scope)
26
+ @arel_node
27
+ end
28
+
29
+ def select_node
30
+ Arel::Nodes::As.new(@arel_node, Arel.sql(@sql_alias))
31
+ end
32
+
33
+ def extract_value(record, _)
34
+ record[@sql_alias]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Types
5
+ class RubyColumn
6
+ attr_reader :loader, :filter, :includes
7
+
8
+ def initialize(loader, key:, filter: nil, includes: nil)
9
+ @loader = loader
10
+ @key = key || :id
11
+ @filter = filter
12
+ @includes = includes
13
+ end
14
+
15
+ def extract_value(record, ruby_data)
16
+ key_value = case @key
17
+ when Proc
18
+ @key.call(record)
19
+ when Array
20
+ @key.reduce(record) { |obj, method| obj.public_send(method) }
21
+ when Symbol
22
+ record.public_send(@key)
23
+ else
24
+ @key
25
+ end
26
+
27
+ ruby_data[@loader]&.[](key_value)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ VERSION = '0.1.0'
5
+ end
data/lib/prato.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "prato/version"
4
+
5
+ require_relative "prato/query/filter"
6
+ require_relative "prato/query/sort"
7
+ require_relative "prato/query/and_filter"
8
+ require_relative "prato/query/or_filter"
9
+ require_relative "prato/query/parameters"
10
+ require_relative "prato/query/field_resolver"
11
+ require_relative "prato/query/default_parser"
12
+
13
+ require_relative "prato/configuration"
14
+
15
+ require_relative "prato/types/association_column"
16
+ require_relative "prato/types/direct_column"
17
+ require_relative "prato/types/expression_column"
18
+ require_relative "prato/types/aggregate_column"
19
+ require_relative "prato/types/ruby_column"
20
+
21
+ require_relative "prato/internal/active_record_version"
22
+ require_relative "prato/internal/lazy_loader_cache"
23
+
24
+ if Prato::Internal::ActiveRecordVersion.legacy?
25
+ require_relative "prato/internal/join_helper_legacy"
26
+ else
27
+ require_relative "prato/internal/join_helper"
28
+ end
29
+
30
+ require_relative "prato/internal/sql_support"
31
+ require_relative "prato/internal/query_state"
32
+ require_relative "prato/internal/specification"
33
+ require_relative "prato/internal/specification_builder"
34
+
35
+ require_relative "prato/internal/pipeline/filtering"
36
+ require_relative "prato/internal/pipeline/pagination"
37
+ require_relative "prato/internal/pipeline/serializer"
38
+ require_relative "prato/internal/pipeline/sorting"
39
+
40
+ require_relative "prato/table"
41
+ require_relative "prato/table_builder"
42
+ require_relative "prato/internal/query_executor"
43
+
44
+ module Prato
45
+ extend self
46
+
47
+ def table(base_model, &block)
48
+ raise ArgumentError, "Prato.table requires a block" unless block_given?
49
+ raise ArgumentError, "Prato.table block must not accept arguments" unless block.parameters.empty?
50
+
51
+ builder = TableBuilder.new
52
+ builder.instance_exec(&block)
53
+
54
+ spec = builder.spec_builder.build(base_model)
55
+
56
+ Table.new(spec)
57
+ end
58
+
59
+ def setup(&block)
60
+ if block_given?
61
+ Configuration.configure(&block)
62
+ else
63
+ Configuration.new
64
+ end
65
+ end
66
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prato
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Valter Santos
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ description: "Prato is a library that simplifies the backend code required to support
27
+ queryable data, \nby mapping parameters onto a table structure, \nallowing Prato
28
+ to invoke Active Record methods like `.where`, `.order`, `.joins`, `.pluck` and
29
+ others.\n\nThe immediate use case for this is fetching data for tables in the frontend,
30
+ \nand with a simple *Prato* table, it becomes trivial to provide any kind of filtering
31
+ / sorting / pagination operations \nover an Active Record relation.\n"
32
+ email:
33
+ - valter@trecitano.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - CHANGELOG.md
39
+ - LICENSE.txt
40
+ - README.md
41
+ - lib/prato.rb
42
+ - lib/prato/configuration.rb
43
+ - lib/prato/internal/active_record_version.rb
44
+ - lib/prato/internal/join_helper.rb
45
+ - lib/prato/internal/join_helper_legacy.rb
46
+ - lib/prato/internal/lazy_loader_cache.rb
47
+ - lib/prato/internal/pipeline/filtering.rb
48
+ - lib/prato/internal/pipeline/pagination.rb
49
+ - lib/prato/internal/pipeline/serializer.rb
50
+ - lib/prato/internal/pipeline/sorting.rb
51
+ - lib/prato/internal/query_executor.rb
52
+ - lib/prato/internal/query_state.rb
53
+ - lib/prato/internal/specification.rb
54
+ - lib/prato/internal/specification_builder.rb
55
+ - lib/prato/internal/sql_support.rb
56
+ - lib/prato/query/and_filter.rb
57
+ - lib/prato/query/default_parser.rb
58
+ - lib/prato/query/field_resolver.rb
59
+ - lib/prato/query/filter.rb
60
+ - lib/prato/query/or_filter.rb
61
+ - lib/prato/query/parameters.rb
62
+ - lib/prato/query/sort.rb
63
+ - lib/prato/table.rb
64
+ - lib/prato/table_builder.rb
65
+ - lib/prato/types/aggregate_column.rb
66
+ - lib/prato/types/association_column.rb
67
+ - lib/prato/types/direct_column.rb
68
+ - lib/prato/types/expression_column.rb
69
+ - lib/prato/types/ruby_column.rb
70
+ - lib/prato/version.rb
71
+ homepage: https://prato.trecitano.com/
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ allowed_push_host: https://rubygems.org
76
+ homepage_uri: https://prato.trecitano.com/
77
+ source_code_uri: https://github.com/trecitano/prato
78
+ changelog_uri: https://github.com/trecitano/prato/releases
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 2.4.0
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 4.0.6
94
+ specification_version: 4
95
+ summary: Filter, sort, and paginate Active Record queries from a table definition.
96
+ test_files: []