praxis 2.0.pre.2 → 2.0.pre.3

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.
@@ -1,10 +1,11 @@
1
1
  require 'active_support/concern'
2
2
 
3
+
3
4
  module Praxis::Mapper
4
5
  module SequelCompat
5
6
  extend ActiveSupport::Concern
6
7
 
7
- `` included do
8
+ included do
8
9
  attr_accessor :_resource
9
10
  end
10
11
 
@@ -13,11 +14,16 @@ module Praxis::Mapper
13
14
  Praxis::Extensions::SequelFilterQueryBuilder
14
15
  end
15
16
 
17
+ def _field_selector_query_builder_class
18
+ Praxis::Extensions::FieldSelection::SequelQuerySelector
19
+ end
20
+
16
21
  def _praxis_associations
17
22
  orig = self.association_reflections.clone
18
-
19
23
  orig.each do |k,v|
20
24
  v[:model] = v.associated_class
25
+ v[:local_key_columns] = local_columns_used_for_the_association(v[:type], v)
26
+ v[:remote_key_columns] = remote_columns_used_for_the_association(v[:type], v)
21
27
  if v.respond_to?(:primary_key)
22
28
  v[:primary_key] = v.primary_key
23
29
  else
@@ -31,7 +37,40 @@ module Praxis::Mapper
31
37
  orig
32
38
  end
33
39
 
40
+ private
41
+ def local_columns_used_for_the_association(type, assoc_reflection)
42
+ case type
43
+ when :one_to_many
44
+ # The associated table (or middle table if many to many) will point to us by PK
45
+ assoc_reflection[:primary_key_columns]
46
+ when :many_to_one
47
+ # We have the FKs to the associated model
48
+ assoc_reflection[:keys]
49
+ when :many_to_many
50
+ # The middle table if many to many) will point to us by key (usually the PK, but not always)
51
+ assoc_reflection[:left_primary_keys]
52
+ else
53
+ raise "association type #{type} not supported"
54
+ end
55
+ end
56
+
57
+ def remote_columns_used_for_the_association(type, assoc_reflection)
58
+ case type
59
+ when :one_to_many
60
+ # The columns in the associated table that will point back to the original association
61
+ assoc_reflection[:keys]
62
+ when :many_to_one
63
+ # The columns in the associated table that the children will point to (usually the PK, but not always) ??
64
+ [assoc_reflection.associated_class.primary_key]
65
+ when :many_to_many
66
+ # The middle table if many to many will point to us by key (usually the PK, but not always) ??
67
+ [assoc_reflection.associated_class.primary_key]
68
+ else
69
+ raise "association type #{type} not supported"
70
+ end
71
+ end
72
+
34
73
  end
35
74
 
36
75
  end
37
- end
76
+ end
@@ -8,6 +8,21 @@ module Praxis
8
8
 
9
9
  class Plugin < Praxis::Plugin
10
10
  include Singleton
11
+
12
+ def config_key
13
+ :mapper
14
+ end
15
+
16
+ def load_config!
17
+ {} # override the default one, since we don't necessarily want to configure it via a yaml file.
18
+ end
19
+
20
+ def prepare_config!(node)
21
+ node.attributes do
22
+ attribute :debug_queries, Attributor::Boolean, default: false,
23
+ description: 'Weather or not to log debug information about queries executed in the build_query automation module'
24
+ end
25
+ end
11
26
  end
12
27
 
13
28
  module Controller
@@ -32,8 +47,7 @@ module Praxis
32
47
  filters = request.params.filters if request.params&.respond_to?(:filters)
33
48
  base_query = domain_model.craft_filter_query( base_query , filters: filters )
34
49
 
35
- resolved = Praxis::MediaType::FieldResolver.resolve(self.media_type, self.expanded_fields)
36
- base_query = domain_model.craft_field_selection_query(base_query, selectors: selector_generator.selectors, resolved: resolved)
50
+ base_query = domain_model.craft_field_selection_query(base_query, selectors: selector_generator.selectors)
37
51
 
38
52
  # TODO: handle pagination and ordering
39
53
  base_query
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '2.0.pre.2'
2
+ VERSION = '2.0.pre.3'
3
3
  end
@@ -51,4 +51,7 @@ Gem::Specification.new do |spec|
51
51
  spec.add_development_dependency 'fuubar', '~> 2'
52
52
  spec.add_development_dependency 'yard', ">= 0.9.20"
53
53
  spec.add_development_dependency 'coveralls'
54
+ # Just for the query selector extensions etc...
55
+ spec.add_development_dependency 'sequel', '~> 5'
56
+ spec.add_development_dependency 'activerecord', '> 4'
54
57
  end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative 'support/spec_resources_active_model.rb'
4
+
5
+ describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
6
+ let(:selector_fields) do
7
+ {
8
+ name: true,
9
+ author: {
10
+ id: true,
11
+ books: true
12
+ },
13
+ category: {
14
+ name: true,
15
+ books: true
16
+ },
17
+ tags: {
18
+ name: true
19
+ }
20
+ }
21
+ end
22
+ let(:expected_select_from_to_query) do
23
+ # The columns to select from the top Simple model
24
+ [
25
+ :simple_name, # from the :name alias
26
+ :author_id, # the FK needed for the author association
27
+ :added_column, # from the extra column defined in the parent property
28
+ :category_uuid, # the FK needed for the cateory association
29
+ :id # We always load the primary keys
30
+ ]
31
+ end
32
+ let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(ActiveBookResource,selector_fields) }
33
+
34
+ subject(:selector) {described_class.new(query: query, selectors: selector_node) }
35
+ context '#generate with a mocked' do
36
+ let(:query) { double("Query") }
37
+ it 'calls the select columns for the top level, and includes the right association hashes' do
38
+ expect(query).to receive(:select).with(*expected_select_from_to_query).and_return(query)
39
+ expected_includes = {
40
+ author: {
41
+ books: {}
42
+ },
43
+ category: {
44
+ books: {}
45
+ },
46
+ tags: {}
47
+ }
48
+ expect(query).to receive(:includes).with(expected_includes).and_return(query)
49
+ expect(subject).to_not receive(:explain_query)
50
+ subject.generate
51
+ end
52
+ it 'calls the explain debug method if enabled' do
53
+ expect(query).to receive(:select).and_return(query)
54
+ expect(query).to receive(:includes).and_return(query)
55
+ expect(subject).to receive(:explain_query)
56
+ subject.generate(debug: true)
57
+ end
58
+ end
59
+
60
+ context '#generate with a real AR model' do
61
+ let(:query) { ActiveBook }
62
+
63
+ it 'calls the select columns for the top level, and includes the right association hashes' do
64
+ expected_includes = {
65
+ author: {
66
+ books: {}
67
+ },
68
+ category: {
69
+ books: {}
70
+ },
71
+ tags: {}
72
+ }
73
+ #expect(query).to receive(:includes).with(expected_includes).and_return(query)
74
+ expect(subject).to_not receive(:explain_query)
75
+ final_query = subject.generate
76
+ expect(final_query.select_values).to match_array(expected_select_from_to_query)
77
+ # Our query selector always uses a single hash tree from the top, not an array of things
78
+ includes_hash = final_query.includes_values.first
79
+ expect(includes_hash).to match(expected_includes)
80
+ # Also, make AR do the actual query to make sure everything is wired up correctly
81
+ result = final_query.to_a
82
+ expect(result.size).to be 2
83
+ book1 = result[0]
84
+ book2 = result[1]
85
+ expect(book1.author.id).to eq 11
86
+ expect(book1.author.books.size).to eq 1
87
+ expect(book1.author.books.map(&:simple_name)).to eq(['Book1'])
88
+ expect(book1.category.name).to eq 'cat1'
89
+ expect(book1.tags.map(&:name)).to match_array(['blue','red'])
90
+
91
+ expect(book2.author.id).to eq 22
92
+ expect(book2.author.books.size).to eq 1
93
+ expect(book2.author.books.map(&:simple_name)).to eq(['Book2'])
94
+ expect(book2.category.name).to eq 'cat2'
95
+ expect(book2.tags.map(&:name)).to match_array(['red'])
96
+ end
97
+
98
+ it 'calls the explain debug method if enabled' do
99
+ suppress_output do
100
+ # Actually make it run all the way...but suppressing the output
101
+ subject.generate(debug: true)
102
+ end
103
+ end
104
+ end
105
+
106
+ end
@@ -0,0 +1,147 @@
1
+ require 'spec_helper'
2
+ require 'sequel'
3
+
4
+ require 'praxis/extensions/field_selection/sequel_query_selector'
5
+
6
+
7
+ describe Praxis::Extensions::FieldSelection::SequelQuerySelector do
8
+ class Q
9
+ attr_reader :object, :cols
10
+ def initialize
11
+ @object = {}
12
+ @cols = []
13
+ end
14
+ def eager(hash)
15
+ raise "we are only calling eager one at a time!" if hash.keys.size > 1
16
+ name = hash.keys.first
17
+ # Actually call the incoming proc with an instance of Q, to collect the further select/eager calls
18
+ @object[name] = hash[name].call(Q.new)
19
+ self
20
+ end
21
+ def select(*names)
22
+ @cols += names.map(&:column)
23
+ self
24
+ end
25
+ def dump
26
+ eagers = @object.each_with_object({}) do |(name, val), hash|
27
+ hash[name] = val.dump
28
+ end
29
+ {
30
+ columns: @cols,
31
+ eagers: eagers
32
+ }
33
+ end
34
+ end
35
+
36
+
37
+ # Pay the price for creating and connecting only in this spec instead in spec helper
38
+ # this way all other specs do not need to be slower and it's a better TDD experience
39
+
40
+ require_relative 'support/spec_resources_sequel.rb'
41
+
42
+ let(:selector_fields) do
43
+ {
44
+ name: true,
45
+ other_model: {
46
+ id: true
47
+ },
48
+ parent: {
49
+ children: true
50
+ },
51
+ tags: {
52
+ tag_name: true
53
+ }
54
+ }
55
+ end
56
+ let(:expected_select_from_to_query) do
57
+ # The columns to select from the top Simple model
58
+ [
59
+ :simple_name, # from the :name alias
60
+ :added_column, # from the extra column defined in the parent property
61
+ :id, # We always load the primary keys
62
+ :other_model_id, # the FK needed for the other_model association
63
+ :parent_id # the FK needed for the parent association
64
+ ]
65
+ end
66
+
67
+ let(:selector_node) { Praxis::Mapper::SelectorGenerator.new.add(SequelSimpleResource,selector_fields) }
68
+ subject {described_class.new(query: query, selectors: selector_node) }
69
+
70
+ context 'generate' do
71
+ context 'using the real models and DB' do
72
+ let(:query) { SequelSimpleModel }
73
+
74
+ it 'calls the select columns for the top level, and includes the right association hashes' do
75
+ ds = subject.generate
76
+ opts = ds.opts
77
+ # Top model is our simplemodel
78
+ expect(opts[:model]).to be(SequelSimpleModel)
79
+ selected_column_names = opts[:select].map(&:column)
80
+ expect(selected_column_names).to match_array(expected_select_from_to_query)
81
+ # 2 Eager loaded associations as well
82
+ expect(opts[:eager].keys).to match_array([:other_model, :parent, :tags])
83
+ # We can not introspect those eagers, as they are procs...but at least validate they are
84
+ expect(opts[:eager][:other_model]).to be_a Proc
85
+ expect(opts[:eager][:parent]).to be_a Proc
86
+
87
+ # Also, let's make sure the query actually works by making Sequel attempt to retrieve it and finding the right things.
88
+ result = ds.all
89
+ # 2 simple models
90
+ expect(result.size).to be 2
91
+ # First simple model points to other_model 11 and parent 1
92
+ simple_one = result.find{|i| i.id == 1}
93
+ expect(simple_one.other_model.id).to be 11
94
+ expect(simple_one.parent.id).to be 1
95
+ # also, its' parent in turn has 2 children (1 and 2) linked by its parent_uuid
96
+ expect(simple_one.parent.children.map(&:id)).to match_array([1,2])
97
+ # Has the blue and red tags
98
+ expect(simple_one.tags.map(&:tag_name)).to match_array(['blue','red'])
99
+
100
+ # second simple model points to other_model 22 and parent 2
101
+ simple_two = result.find{|i| i.id == 2}
102
+ expect(simple_two.other_model.id).to be 22
103
+ expect(simple_two.parent.id).to be 2
104
+ # also, its' parent in turn has no children (as no simple models point to it by uuid)
105
+ expect(simple_two.parent.children.map(&:id)).to be_empty
106
+ # Also has the red tag
107
+ expect(simple_two.tags.map(&:tag_name)).to match_array(['red'])
108
+ end
109
+ it 'calls the explain debug method if enabled' do
110
+ suppress_output do
111
+ # Actually make it run all the way...but suppressing the output
112
+ subject.generate(debug: true)
113
+ end
114
+ end
115
+ end
116
+ context 'just mocking the query' do
117
+ let(:query) { Q.new }
118
+
119
+ it 'creates the right recursive lambdas for the eager loading' do
120
+
121
+ ds = subject.generate
122
+ result = ds.dump
123
+ expect(result[:columns]).to match_array(expected_select_from_to_query)
124
+ # 2 eager loads
125
+ expect(result[:eagers].keys).to match_array([:other_model, :parent, :tags])
126
+ # 1 - other model
127
+ other_model_eagers = result[:eagers][:other_model]
128
+ expect(other_model_eagers[:columns]).to match_array([:id])
129
+
130
+ # 2 - parent association
131
+ parent_eagers = result[:eagers][:parent]
132
+ expect(parent_eagers[:columns]).to match_array([:id,:uuid]) # uuid is necessary for the "children" assoc
133
+ expect(parent_eagers[:eagers].keys).to match_array([:children])
134
+ # 2.1 - children association off of the parent
135
+ parent_children_eagers = parent_eagers[:eagers][:children]
136
+ expect(parent_children_eagers[:columns]).to match_array([:id,:parent_uuid]) # parent_uuid is required for the assoc
137
+ expect(parent_children_eagers[:eagers]).to be_empty
138
+
139
+ # 3 - tags association
140
+ tags_eagers = result[:eagers][:tags]
141
+ expect(tags_eagers[:columns]).to match_array([:id, :tag_name]) # uuid is necessary for the "children" assoc
142
+ expect(tags_eagers[:eagers].keys).to be_empty
143
+
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,130 @@
1
+ require 'active_record'
2
+
3
+ require 'praxis/mapper/active_model_compat'
4
+
5
+ # Creates a new in-memory DB, and the necessary tables (and mini-seeds) for the models in this file
6
+ def create_tables
7
+
8
+ ActiveRecord::Base.establish_connection(
9
+ adapter: 'sqlite3',
10
+ dbfile: ':memory:',
11
+ database: ':memory:'
12
+ )
13
+
14
+ ActiveRecord::Schema.define do
15
+ ActiveRecord::Migration.suppress_messages do
16
+ create_table :active_books do |table|
17
+ table.column :simple_name, :string
18
+ table.column :added_column, :string
19
+ table.column :category_uuid, :string
20
+ table.column :author_id, :integer
21
+ end
22
+
23
+ create_table :active_authors do |table|
24
+ table.column :name, :string
25
+ end
26
+
27
+ create_table :active_categories do |table|
28
+ table.column :uuid, :string
29
+ table.column :name, :string
30
+ end
31
+
32
+ create_table :active_tags do |table|
33
+ table.column :name, :string
34
+ end
35
+
36
+ create_table :active_taggings do |table|
37
+ table.column :book_id, :integer
38
+ table.column :tag_id, :integer
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ create_tables
45
+
46
+ class ActiveBook < ActiveRecord::Base
47
+ include Praxis::Mapper::ActiveModelCompat
48
+
49
+ belongs_to :category, class_name: 'ActiveCategory', foreign_key: :category_uuid, primary_key: :uuid
50
+ belongs_to :author, class_name: 'ActiveAuthor'
51
+ has_many :taggings, class_name: 'ActiveTagging', foreign_key: :book_id
52
+ has_many :tags, class_name: 'ActiveTag', through: :taggings
53
+ end
54
+
55
+ class ActiveAuthor < ActiveRecord::Base
56
+ include Praxis::Mapper::ActiveModelCompat
57
+ has_many :books, class_name: 'ActiveBook', foreign_key: :author_id
58
+ end
59
+
60
+ class ActiveCategory < ActiveRecord::Base
61
+ include Praxis::Mapper::ActiveModelCompat
62
+ has_many :books, class_name: 'ActiveBook', primary_key: :uuid, foreign_key: :category_uuid
63
+ end
64
+
65
+ class ActiveTag < ActiveRecord::Base
66
+ include Praxis::Mapper::ActiveModelCompat
67
+ end
68
+
69
+ class ActiveTagging < ActiveRecord::Base
70
+ include Praxis::Mapper::ActiveModelCompat
71
+ belongs_to :book, class_name: 'ActiveBook', foreign_key: :book_id
72
+ belongs_to :tag, class_name: 'ActiveTag', foreign_key: :tag_id
73
+ end
74
+
75
+
76
+ # A set of resource classes for use in specs
77
+ class ActiveBaseResource < Praxis::Mapper::Resource
78
+ end
79
+
80
+ class ActiveAuthorResource < ActiveBaseResource
81
+ model ActiveAuthor
82
+
83
+ property :display_name, dependencies: [:name]
84
+ end
85
+
86
+ class ActiveCategoryResource < ActiveBaseResource
87
+ model ActiveCategory
88
+ end
89
+
90
+ class ActiveTagResource < ActiveBaseResource
91
+ model ActiveTag
92
+ end
93
+
94
+ class ActiveBookResource < ActiveBaseResource
95
+ model ActiveBook
96
+
97
+ # Forces to add an extra column (added_column)...and yet another (author_id) that will serve
98
+ # to check that if that's already automatically added due to an association, it won't interfere or duplicate
99
+ property :author, dependencies: [:author, :added_column, :author_id]
100
+
101
+ property :name, dependencies: [:simple_name]
102
+ end
103
+
104
+
105
+ def seed_data
106
+ cat1 = ActiveCategory.create( id: 1 , uuid: 'deadbeef1', name: 'cat1' )
107
+ cat2 = ActiveCategory.create( id: 2 , uuid: 'deadbeef2', name: 'cat2' )
108
+
109
+ author1 = ActiveAuthor.create( id: 11, name: 'author1' )
110
+ author2 = ActiveAuthor.create( id: 22, name: 'author2' )
111
+
112
+ tag_blue = ActiveTag.create(id: 1 , name: 'blue' )
113
+ tag_red = ActiveTag.create(id: 2 , name: 'red' )
114
+
115
+ book1 = ActiveBook.create( id: 1 , simple_name: 'Book1', category_uuid: 'deadbeef1')
116
+ book1.author = author1
117
+ book1.category = cat1
118
+ book1.save
119
+ ActiveTagging.create(book: book1, tag: tag_blue)
120
+ ActiveTagging.create(book: book1, tag: tag_red)
121
+
122
+
123
+ book2 = ActiveBook.create( id: 2 , simple_name: 'Book2', category_uuid: 'deadbeef1')
124
+ book2.author = author2
125
+ book2.category = cat2
126
+ book2.save
127
+ ActiveTagging.create(book: book2, tag: tag_red)
128
+ end
129
+
130
+ seed_data