praxis 2.0.pre.10 → 2.0.pre.11

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -3
  3. data/CHANGELOG.md +9 -0
  4. data/bin/praxis +59 -2
  5. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  6. data/lib/praxis/docs/open_api_generator.rb +1 -1
  7. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +57 -8
  8. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  9. data/lib/praxis/extensions/pagination.rb +5 -32
  10. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  11. data/lib/praxis/mapper/resource.rb +18 -2
  12. data/lib/praxis/mapper/selector_generator.rb +1 -0
  13. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  14. data/lib/praxis/plugins/mapper_plugin.rb +22 -13
  15. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  16. data/lib/praxis/tasks/api_docs.rb +4 -1
  17. data/lib/praxis/version.rb +1 -1
  18. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +15 -2
  19. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  20. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  21. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  22. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  23. data/tasks/thor/example.rb +12 -6
  24. data/tasks/thor/model.rb +40 -0
  25. data/tasks/thor/scaffold.rb +117 -0
  26. data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
  27. data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
  28. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  29. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
  30. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +11 -0
  31. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
  32. data/tasks/thor/templates/generator/example_app/config.ru +1 -2
  33. data/tasks/thor/templates/generator/example_app/config/environment.rb +2 -1
  34. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
  35. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  36. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
  37. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
  38. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
  39. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
  40. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
  41. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  42. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  43. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  44. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  45. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  46. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  47. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  48. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd76d8bd13954d5607d009ee37ff7fc1a8ef8a2c6c8c87544dddaee332d984bf
4
- data.tar.gz: 719bffed7ea98fe42d7c620cd4377c4201c9d23e49a9777e981647d671b39ffd
3
+ metadata.gz: 7bbd374311046cf8d12c68d564382de6cbfd0c1032c45bbc8c1ac00d7a02e68a
4
+ data.tar.gz: e33676f45266facdbcae595c2c05ca94ae8dfa34134aa9e4113e5a5d9d729052
5
5
  SHA512:
6
- metadata.gz: 18e87e4563faa7a1ffbba174634f1ebabe57320b8324e4ac23a711f5785906482412db711c83308ff87264d48cc496f8bd118b5a5ac77f1d6bd8f09e543f90ed
7
- data.tar.gz: 9da869a6acecf7c04273ef7dc0519be0cf8b3d00c29df16c851df04f5ea52d7cf4d62123e25533b8887c0a30b940155abfbd812f4fe2b347d1139df91e3e124f
6
+ metadata.gz: 5db86e95bd0b723560036435ded99b893fd92bd55f20dc4b49e326d96e7c9422549124ba986eb3248ff746d6bf928a60e6677b854c8761a34c026c7430a578bb
7
+ data.tar.gz: 39677f252617f79d3d054fd2286562d3355310d8161096c7c73294d2b242cee4ebdf749b462d7f1c5261fc487e80ba473dcd258dcb77a510c47db7e698275cad
data/.travis.yml CHANGED
@@ -1,10 +1,8 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.4
5
- - 2.5
6
4
  - 2.6
7
- - 2.7
5
+ - 2.7
8
6
  script:
9
7
  - bundle exec rspec spec
10
8
  branches:
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 2.0.pre.11
6
+
7
+ - Remove MapperPlugin's `set_selectors` (made `selector_generator` lazy instead), and ensure it includes the rendering extensions to the Controllers. Less things to configure if you opt into the Mapper way.
8
+ - Built scaffolding generator for quickly creating a new API endpoint in the praxis binary (it builds endpoint+mediatype+controller+resource at one, with useful base code and comments)
9
+ - Dropped support for Ruby 2.4 and 2.5 as some of the newest dependent gems are dropping it as well.
10
+ - Simplify filters_mapping definition, by not requiring to define same-name mappings if the underlying model has an attribute with the same exact name. i.e., a `name: :name` entry is not necessary if the model has a `:name` attribute.
11
+
12
+ ## 2.0.pre.10
13
+
5
14
  - Simple, but pervasive breaking change: Rename `ResourceDefinition` to `EndpointDefinition` (but same functionality).
6
15
  - Remove all deprecated features (and raise error describing it's not supported yet)
7
16
  - Remove `Links` and `LinkBuilder`. Those seem unnecessary from a Framework point of view as they aren't clear most
data/bin/praxis CHANGED
@@ -46,7 +46,7 @@ class PraxisGenerator < Thor
46
46
  def routes
47
47
  end
48
48
 
49
- desc "docs [generate|browser|package]", <<-EOF
49
+ desc "docs [generate|browser|package]", <<~EOF
50
50
  Generates API documentation and a Web App to inspect it
51
51
  generate - Generates the JSON docs
52
52
  browser - (default) Generates JSON docs, and automatically starts a Web app to browse them.
@@ -81,7 +81,64 @@ class PraxisGenerator < Thor
81
81
  gen = ::PraxisGen::Example.new([app_name])
82
82
  gen.destination_root = app_name
83
83
  gen.invoke(:example)
84
- end
84
+ end
85
+
86
+ desc_for "g COLLECTION_NAME", ::PraxisGen::Scaffold, :g
87
+ # Cannot use the argument below or it will apply to all commands (the action in the class has it)
88
+ # argument :collection_name, required: false
89
+ # The options, however, since they're optional are fine (But need to be duplicated from the class :( )
90
+ option :version, required: false, default: '1',
91
+ desc: 'Version string for the API endpoint. This also dictates the directory structure (i.e., v1/endpoints/...))'
92
+ option :design, type: :boolean, default: true,
93
+ desc: 'Include the Endpoint and MediaType files for the collection'
94
+ option :implementation, type: :boolean, default: true,
95
+ desc: 'Include the Controller and (possibly the) Resource files for the collection (see --no-resource)'
96
+ option :resource, type: :boolean, default: true,
97
+ desc: 'Disable (or enable) the creation of the Resource files when generating implementation'
98
+ option :model, type: :string, enum: ['activerecord','sequel'],
99
+ desc: 'It also generates a model for the given ORM. An empty --model flag will default to activerecord'
100
+ option :actions, type: :string, default: 'crud', enum: ['cr','cru','crud','u','ud','d'],
101
+ desc: 'Specifies the actions to generate for the API. cr=create, u=update, d=delete. Index and show actions are always generated'
102
+ def g(*args)
103
+ # Because we cannot share the :collection_name argument, we need to do this check here, before
104
+ # we "parse" it and pass it to the g command
105
+ unless args.size == 1
106
+ ::PraxisGen::Scaffold.command_help(shell,:g)
107
+ exit 1
108
+ end
109
+
110
+ collection_name,_ = args
111
+ ::PraxisGen::Scaffold.new([collection_name],options).invoke(:g)
112
+ if options[:model]
113
+ # Make it easy to be able to both enable or not enable the creation of the model, by passing --model=...
114
+ # but also make it easy so that if there is no value for it, it default to activerecord
115
+ opts = {orm: options[:model] }
116
+ opts[:orm] = 'activerecord' if opts[:orm] == 'model' # value is model param passed by no value
117
+ ::PraxisGen::Model.new([collection_name.singularize],opts).invoke(:g)
118
+ end
119
+ end
120
+
121
+ # Initially, the idea was to build some quick model generator, but I think it's better to keep it
122
+ # simple and just use the scaffold generator with `--no-implementation --no-design --model` instead
123
+ # Left here in case we want to rescue it
124
+ # desc_for "gmodel MODEL_NAME", ::PraxisGen::Model, :g
125
+ # # Cannot use the argument below or it will apply to all commands (the action in the class has it)
126
+ # # argument :collection_name, required: false
127
+ # # The options, however, since they're optional are fine (But need to be duplicated from the class :( )
128
+ # option :orm, required: false, default: 'activerecord', enum: ['activerecord','sequel'],
129
+ # desc: 'Type of ORM model to create.'
130
+ # def gmodel(*args)
131
+ # # Because we cannot share the :collection_name argument, we need to do this check here, before
132
+ # # we "parse" it and pass it to the g command
133
+ # unless args.size == 1
134
+ # ::PraxisGen::Model.command_help(shell,:g)
135
+ # exit 1
136
+ # end
137
+
138
+ # model_name,_ = args
139
+ # ::PraxisGen::Model.new([model_name],options).invoke(:g)
140
+ # end
141
+
85
142
  end
86
143
 
87
144
  PraxisGenerator.start(ARGV)
@@ -33,6 +33,7 @@ module Praxis
33
33
  map :models, 'models/**/*'
34
34
  map :responses, '**/responses/**/*'
35
35
  map :exceptions, '**/exceptions/**/*'
36
+ map :concerns, '**/concerns/**/*'
36
37
  map :resources, '**/resources/**/*'
37
38
  map :controllers, '**/controllers/**/*'
38
39
  end
@@ -244,7 +244,7 @@ module Praxis
244
244
  resources_by_version.keys.each do |version|
245
245
  FileUtils.mkdir_p @doc_root_dir + '/' + version
246
246
  end
247
- FileUtils.mkdir_p @doc_root_dir + '/unversioned'
247
+ FileUtils.mkdir_p @doc_root_dir + '/unversioned' if resources_by_version.keys.include?('n/a')
248
248
  end
249
249
 
250
250
  def normalize_media_types( mtis )
@@ -5,27 +5,43 @@ module Praxis
5
5
  module AttributeFiltering
6
6
  ALIAS_TABLE_PREFIX = ''
7
7
  require_relative 'active_record_patches'
8
+ # Helper class that can present an SqlLiteral string which we have already quoted
9
+ # ... but! that can properly provide a "to_sym" that has the value unquoted
10
+ # This is necessary as (the latest AR code):
11
+ # * does not carry over "references" in joins if they are not SqlLiterals
12
+ # * but, at the same time, it indexes the references using the .to_sym value (which is really expected to be the normal string, without quotes)
13
+ # If we pass a normal SqlLiteral, instead of our wrapper, without quoting the table, the current AR code will never quote it to form the
14
+ # SQL string, as it's already a literal...so our "/" type separators as names won't work without quoting.
15
+ class QuasiSqlLiteral < Arel::Nodes::SqlLiteral
16
+ def initialize(quoted:, symbolized:)
17
+ @symbolized = symbolized
18
+ super(quoted)
19
+ end
20
+ def to_sym
21
+ @symbolized
22
+ end
23
+ end
8
24
 
9
25
  class ActiveRecordFilterQueryBuilder
10
- attr_reader :query, :model, :attr_to_column
26
+ attr_reader :query, :model, :filters_map
11
27
 
12
28
  # Base query to build upon
13
29
  def initialize(query: , model:, filters_map:, debug: false)
14
30
  @query = query
15
31
  @model = model
16
- @attr_to_column = filters_map
32
+ @filters_map = filters_map
17
33
  @logger = debug ? Logger.new(STDOUT) : nil
18
34
  end
19
35
 
20
- def debug(msg)
21
- @logger && @logger.info(msg)
36
+ def debug_query(msg, query)
37
+ @logger.info(msg + query.to_sql) if @logger
22
38
  end
23
39
 
24
40
  def generate(filters)
25
41
  # Resolve the names and values first, based on filters_map
26
42
  root_node = _convert_to_treenode(filters)
27
43
  craft_filter_query(root_node, for_model: @model)
28
- debug("SQL due to filters: #{@query.all.to_sql}")
44
+ debug_query("SQL due to filters: ", @query.all)
29
45
  @query
30
46
  end
31
47
 
@@ -45,13 +61,30 @@ module Praxis
45
61
 
46
62
  private
47
63
 
64
+ def _mapped_filter(name)
65
+ target = @filters_map[name]
66
+ unless target
67
+ if @model.attribute_names.include?(name.to_s)
68
+ # Cache it in the filters mapping (to avoid later lookups), and return it.
69
+ @filters_map[name] = name
70
+ target = name
71
+ end
72
+ end
73
+ return target
74
+ end
75
+
48
76
  # Resolve and convert from filters, to a more manageable and param-type-independent structure
49
77
  def _convert_to_treenode(filters)
50
78
  # Resolve the names and values first, based on filters_map
51
79
  resolved_array = []
52
80
  filters.parsed_array.each do |filter|
53
- mapped_value = attr_to_column[filter[:name]]
54
- raise "Filtering by #{filter[:name]} not allowed (no mapping found)" unless mapped_value
81
+ mapped_value = _mapped_filter(filter[:name])
82
+ unless mapped_value
83
+ msg = "Filtering by #{filter[:name]} is not allowed. No implementation mapping defined for it has been found \
84
+ and there is not a model attribute with this name either.\n" \
85
+ "Please add a mapping for #{filter[:name]} in the `filters_mapping` method of the appropriate Resource class"
86
+ raise msg
87
+ end
55
88
  bindings_array = \
56
89
  if mapped_value.is_a?(Proc)
57
90
  result = mapped_value.call(filter)
@@ -85,7 +118,7 @@ module Praxis
85
118
  end
86
119
 
87
120
  def add_clause(column_prefix:, column_object:, op:, value:)
88
- @query = @query.references(column_prefix) #Mark where clause referencing the appropriate alias
121
+ @query = @query.references(build_reference_value(column_prefix)) #Mark where clause referencing the appropriate alias
89
122
  likeval = get_like_value(value)
90
123
  case op
91
124
  when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
@@ -165,6 +198,22 @@ module Praxis
165
198
  likeval
166
199
  end
167
200
  end
201
+
202
+ # The value that we need to stick in the references method is different in the latest Rails
203
+ maj, min, _ = ActiveRecord.gem_version.segments
204
+ if maj == 5 || (maj == 6 && min == 0)
205
+ # In AR 6 (and 6.0) the references are simple strings
206
+ def build_reference_value(column_prefix)
207
+ column_prefix
208
+ end
209
+ else
210
+ # The latest AR versions discard passing references to joins when they're not SqlLiterals ... so let's wrap it
211
+ # with our class, so that it is a literal (already quoted), but that can still provide the expected "symbol" without quotes
212
+ # so that our aliasing code can match it.
213
+ def build_reference_value(column_prefix)
214
+ QuasiSqlLiteral.new(quoted: query.connection.quote_table_name(column_prefix), symbolized: column_prefix.to_sym)
215
+ end
216
+ end
168
217
  end
169
218
  end
170
219
  end
@@ -9,7 +9,7 @@ module Praxis
9
9
  class << self
10
10
  def for(definition)
11
11
  Class.new(self) do
12
- @attr_to_column = case definition
12
+ @filters_map = case definition
13
13
  when Hash
14
14
  definition
15
15
  when Array
@@ -18,7 +18,7 @@ module Praxis
18
18
  raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
19
19
  end
20
20
  class << self
21
- attr_reader :attr_to_column
21
+ attr_reader :filters_map
22
22
  end
23
23
  end
24
24
  end
@@ -33,13 +33,18 @@ module Praxis
33
33
  end
34
34
 
35
35
  # By default we'll simply use the incoming op and value, and will map
36
- # the attribute based on what's on the `attr_to_column` hash
36
+ # the attribute based on what's on the `filters_map` definition
37
37
  def generate(filters)
38
38
  raise "Not refactored yet!"
39
39
  seen_associations = Set.new
40
40
  filters.each do |(attr, spec)|
41
- column_name = attr_to_column[attr]
42
- raise "Filtering by #{attr} not allowed (no mapping found)" unless column_name
41
+ column_name = _mapped_filter(attr)
42
+ unless column_name
43
+ msg = "Filtering by #{attr} is not allowed. No implementation mapping defined for it has been found \
44
+ and there is not a model attribute with this name either.\n" \
45
+ "Please add a mapping for #{attr} in the `filters_mapping` method of the appropriate Resource class"
46
+ raise msg
47
+ end
43
48
  if column_name.is_a?(Proc)
44
49
  bindings = column_name.call(spec)
45
50
  # A hash of bindings, consisting of a key with column name and a value to the query value
@@ -64,9 +69,16 @@ module Praxis
64
69
  add_clause(attr: column_name, op: op, value: value)
65
70
  end
66
71
 
67
- def attr_to_column
68
- # Class method defined by the subclassing Class (using .for)
69
- self.class.attr_to_column
72
+ def _mapped_filter(name)
73
+ target = self.class.filters_map[name]
74
+ unless target
75
+ if @model.attribute_names.include?(name.to_s)
76
+ # Cache it in the filters mapping (to avoid later lookups), and return it.
77
+ self.class.filters_map[name] = name
78
+ target = name
79
+ end
80
+ end
81
+ return target
70
82
  end
71
83
 
72
84
  # Private to try to funnel all column names through `generate` that restricts
@@ -14,10 +14,9 @@ module Praxis
14
14
  module Pagination
15
15
  extend ActiveSupport::Concern
16
16
  # This PaginatedController concern should be added to controllers that have actions that define the
17
- # pagination and order parameters so that calling `paginate( query: <base_query>, table: <main_table_name> )`
18
- # would handle all the required logic for paginating, ordering and generating the Link and TotalCount headers.
19
- # This assumes that the query object are chainable and based on ActiveRecord at the moment (although that logic)
20
- # can be easily applied to other chainable query proxies.
17
+ # pagination and order parameters so that one can call the domain model to craft the query
18
+ # `domain_model.craft_pagination_query(base_query, pagination: _pagination)`
19
+ # This will handle all the required logic for paginating, ordering and generating the Link and TotalCount headers.
21
20
  #
22
21
  # Here's a simple example on how to use it for a fake Items controller
23
22
  # class Items < V1::Controllers::BaseController
@@ -29,7 +28,8 @@ module Praxis
29
28
  #
30
29
  # def index(filters: nil, pagination: nil, order: nil, **_args)
31
30
  # items = current_user.items.all
32
- # items = _craft_pagination_query( query: items)
31
+ # domain_model = self.media_type.domain_model
32
+ # items = domain_model.craft_pagination_query( query: items, pagination: _pagination)
33
33
  #
34
34
  # display(items)
35
35
  # end
@@ -71,33 +71,6 @@ module Praxis
71
71
  @_pagination = PaginationStruct.new(pagination[:paginator], pagination[:order])
72
72
  end
73
73
 
74
- # Main entrypoint: Handles all pagination pieces
75
- # takes:
76
- # * the query to build from and the table
77
- # * the request (for link header generation)
78
- # * requires the _pagination variable to be there (set by this module) to return the pagination struct
79
- def _craft_pagination_query(query:, type: :active_record)
80
- handler_klass = \
81
- case type
82
- when :active_record
83
- ActiveRecordPaginationHandler
84
- when :sequel
85
- SequelPaginationHandler
86
- else
87
- raise "Attempting to use pagination but Active Record or Sequel gems found"
88
- end
89
-
90
- # Gather and save the count if required
91
- if _pagination.paginator&.total_count
92
- _pagination.total_count = handler_klass.count(query.dup)
93
- end
94
-
95
- query = handler_klass.order(query, _pagination.order)
96
- # Maybe this is a class instance instead of a class method?...(of the appropriate AR/Sequel type)...
97
- # self.class.paginate(query, table, _pagination)
98
- handler_klass.paginate(query, _pagination)
99
- end
100
-
101
74
  def build_pagination_headers(pagination:, current_url:, current_query_params:)
102
75
  links = if pagination.paginator.by
103
76
  # We're assuming that the last element has a "symbol/string" field with the same name of the "by" pagination.
@@ -23,6 +23,10 @@ module Praxis
23
23
  Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector
24
24
  end
25
25
 
26
+ def _pagination_query_builder_class
27
+ Praxis::Extensions::Pagination::ActiveRecordPaginationHandler
28
+ end
29
+
26
30
  def _praxis_associations
27
31
  orig = self.reflections.clone
28
32
 
@@ -30,6 +30,7 @@ module Praxis::Mapper
30
30
  end
31
31
 
32
32
  @properties = self.superclass.properties.clone
33
+ @_filters_map = {}
33
34
  end
34
35
 
35
36
  end
@@ -197,7 +198,7 @@ module Praxis::Mapper
197
198
 
198
199
  # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
199
200
  # ...maybe what this means is that we can change it for a better DSL in the resource?
200
- def self.filters_mapping(definition)
201
+ def self.filters_mapping(definition={})
201
202
  @_filters_map = \
202
203
  case definition
203
204
  when Hash
@@ -211,7 +212,9 @@ module Praxis::Mapper
211
212
 
212
213
  def self.craft_filter_query(base_query, filters:) # rubocop:disable Metrics/AbcSize
213
214
  if filters
214
- raise "Must define the mapping of filters if want to use Filtering for resource: #{self}" unless @_filters_map
215
+ unless @_filters_map
216
+ raise "To use API filtering, you must define the mapping of api-names to resource properties (using the `filters_mapping` method in #{self})"
217
+ end
215
218
  debug = Praxis::Application.instance.config.mapper.debug_queries
216
219
  base_query = model._filter_query_builder_class.new(query: base_query, model: model, filters_map: @_filters_map, debug: debug).generate(filters)
217
220
  end
@@ -228,6 +231,19 @@ module Praxis::Mapper
228
231
  base_query
229
232
  end
230
233
 
234
+ def self.craft_pagination_query(base_query, pagination: ) # rubocop:disable Metrics/AbcSize
235
+ handler_klass = model._pagination_query_builder_class
236
+ return base_query unless (handler_klass && (pagination.paginator || pagination.order))
237
+
238
+ # Gather and save the count if required
239
+ if pagination.paginator&.total_count
240
+ pagination.total_count = handler_klass.count(base_query.dup)
241
+ end
242
+
243
+ base_query = handler_klass.order(base_query, pagination.order)
244
+ handler_klass.paginate(base_query, pagination)
245
+ end
246
+
231
247
  def initialize(record)
232
248
  @record = record
233
249
  end
@@ -141,6 +141,7 @@ module Praxis::Mapper
141
141
  def add(resource, fields)
142
142
  @root = SelectorGeneratorNode.new(resource)
143
143
  @root.add(fields)
144
+ self
144
145
  end
145
146
 
146
147
  def selectors
@@ -7,6 +7,9 @@ module Praxis::Mapper
7
7
 
8
8
  included do
9
9
  attr_accessor :_resource
10
+ class <<self
11
+ alias_method :find_by, :find # Easy way to be method compatible with AR
12
+ end
10
13
  end
11
14
 
12
15
  module ClassMethods
@@ -19,6 +22,10 @@ module Praxis::Mapper
19
22
  Praxis::Extensions::FieldSelection::SequelQuerySelector
20
23
  end
21
24
 
25
+ def _pagination_query_builder_class
26
+ Praxis::Extensions::Pagination::SequelPaginationHandler
27
+ end
28
+
22
29
  def _praxis_associations
23
30
  orig = self.association_reflections.clone
24
31
  orig.each do |k,v|