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

Sign up to get free protection for your applications and to get access to all the features.
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|