api_maker 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/app/api_maker/api_helpers/api_maker_helpers.rb +5 -0
  3. data/app/api_maker/services/can_can/load_abilities.rb +30 -0
  4. data/app/api_maker/services/devise/sign_in.rb +64 -0
  5. data/app/api_maker/services/devise/sign_out.rb +9 -0
  6. data/app/api_maker/services/models/find_or_create_by.rb +18 -0
  7. data/app/channels/api_maker/subscriptions_channel.rb +33 -2
  8. data/app/controllers/api_maker/base_controller.rb +7 -3
  9. data/app/controllers/api_maker/commands_controller.rb +26 -4
  10. data/app/controllers/api_maker/session_statuses_controller.rb +1 -1
  11. data/app/services/api_maker/abilities_loader.rb +104 -0
  12. data/app/services/api_maker/application_service.rb +2 -1
  13. data/app/services/api_maker/base_command.rb +248 -0
  14. data/app/services/api_maker/collection_command_service.rb +29 -15
  15. data/app/services/api_maker/collection_loader.rb +124 -0
  16. data/app/services/api_maker/command_failed_error.rb +3 -0
  17. data/app/services/api_maker/command_response.rb +17 -6
  18. data/app/services/api_maker/command_service.rb +3 -3
  19. data/app/services/api_maker/create_command.rb +11 -26
  20. data/app/services/api_maker/create_command_service.rb +3 -3
  21. data/app/services/api_maker/database_type.rb +9 -0
  22. data/app/services/api_maker/deep_merge_params.rb +26 -0
  23. data/app/services/api_maker/deserializer.rb +35 -0
  24. data/app/services/api_maker/destroy_command.rb +15 -21
  25. data/app/services/api_maker/destroy_command_service.rb +3 -3
  26. data/app/services/api_maker/generate_react_native_api_service.rb +3 -19
  27. data/app/services/api_maker/include_helpers.rb +17 -0
  28. data/app/services/api_maker/index_command.rb +8 -88
  29. data/app/services/api_maker/index_command_service.rb +5 -5
  30. data/app/services/api_maker/js_method_namer_service.rb +1 -1
  31. data/app/services/api_maker/locals_from_controller.rb +14 -0
  32. data/app/services/api_maker/member_command_service.rb +15 -13
  33. data/app/services/api_maker/model_classes_java_script_generator_service.rb +37 -0
  34. data/app/services/api_maker/model_content_generator_service.rb +17 -21
  35. data/app/services/api_maker/models/save.rb +29 -0
  36. data/app/services/api_maker/models_finder_service.rb +6 -2
  37. data/app/services/api_maker/models_generator_service.rb +6 -43
  38. data/app/services/api_maker/move_components_to_routes.rb +50 -0
  39. data/app/services/api_maker/primary_id_for_model.rb +6 -0
  40. data/app/services/api_maker/reset_indexed_db_service.rb +36 -0
  41. data/app/services/api_maker/routes_file_reloader.rb +20 -0
  42. data/app/services/api_maker/select_columns_on_collection.rb +78 -0
  43. data/app/services/api_maker/select_parser.rb +32 -0
  44. data/app/services/api_maker/service_command.rb +27 -0
  45. data/app/services/api_maker/service_command_service.rb +14 -0
  46. data/app/services/api_maker/simple_model_errors.rb +52 -0
  47. data/app/services/api_maker/update_command.rb +8 -24
  48. data/app/services/api_maker/update_command_service.rb +3 -3
  49. data/app/services/api_maker/valid_command.rb +4 -13
  50. data/app/services/api_maker/valid_command_service.rb +3 -3
  51. data/app/services/api_maker/validation_errors_generator_service.rb +146 -0
  52. data/app/views/api_maker/_data.html.erb +17 -11
  53. data/config/routes.rb +0 -2
  54. data/lib/api_maker/ability.rb +22 -7
  55. data/lib/api_maker/ability_loader.rb +9 -6
  56. data/lib/api_maker/base_collection_instance.rb +15 -0
  57. data/lib/api_maker/base_resource.rb +135 -9
  58. data/lib/api_maker/base_service.rb +14 -0
  59. data/lib/api_maker/collection_serializer.rb +95 -34
  60. data/lib/api_maker/command_spec_helper.rb +41 -11
  61. data/lib/api_maker/configuration.rb +31 -4
  62. data/lib/api_maker/expect_to_able_to_helper.rb +31 -0
  63. data/lib/api_maker/individual_command.rb +24 -9
  64. data/lib/api_maker/javascript/model-template.js.erb +39 -25
  65. data/lib/api_maker/javascript/models.js.erb +6 -0
  66. data/lib/api_maker/loader.rb +1 -1
  67. data/lib/api_maker/memory_storage.rb +1 -1
  68. data/lib/api_maker/model_extensions.rb +34 -18
  69. data/lib/api_maker/permitted_params_argument.rb +5 -1
  70. data/lib/api_maker/preloader.rb +71 -32
  71. data/lib/api_maker/preloader_base.rb +108 -0
  72. data/lib/api_maker/preloader_belongs_to.rb +34 -33
  73. data/lib/api_maker/preloader_has_many.rb +45 -39
  74. data/lib/api_maker/preloader_has_one.rb +30 -47
  75. data/lib/api_maker/railtie.rb +3 -11
  76. data/lib/api_maker/relationship_preloader.rb +42 -0
  77. data/lib/api_maker/resource_routing.rb +18 -4
  78. data/lib/api_maker/result_parser.rb +34 -20
  79. data/lib/api_maker/serializer.rb +53 -22
  80. data/lib/api_maker/spec_helper/browser_logs.rb +14 -0
  81. data/lib/api_maker/spec_helper/execute_collection_command.rb +46 -0
  82. data/lib/api_maker/spec_helper/execute_member_command.rb +52 -0
  83. data/lib/api_maker/spec_helper/expect_no_browser_errors.rb +18 -0
  84. data/lib/api_maker/spec_helper/wait_for_expect.rb +20 -0
  85. data/lib/api_maker/spec_helper/wait_for_flash_message.rb +21 -0
  86. data/lib/api_maker/spec_helper.rb +112 -48
  87. data/lib/api_maker/version.rb +1 -1
  88. data/lib/api_maker.rb +7 -3
  89. metadata +108 -89
  90. data/README.md +0 -476
  91. data/app/controllers/api_maker/devise_controller.rb +0 -60
  92. data/lib/api_maker/base_command.rb +0 -81
  93. data/lib/api_maker/javascript/api.js +0 -92
  94. data/lib/api_maker/javascript/base-model.js +0 -543
  95. data/lib/api_maker/javascript/bootstrap/attribute-row.jsx +0 -16
  96. data/lib/api_maker/javascript/bootstrap/attribute-rows.jsx +0 -47
  97. data/lib/api_maker/javascript/bootstrap/card.jsx +0 -79
  98. data/lib/api_maker/javascript/bootstrap/checkbox.jsx +0 -127
  99. data/lib/api_maker/javascript/bootstrap/checkboxes.jsx +0 -105
  100. data/lib/api_maker/javascript/bootstrap/live-table.jsx +0 -168
  101. data/lib/api_maker/javascript/bootstrap/money-input.jsx +0 -136
  102. data/lib/api_maker/javascript/bootstrap/radio-buttons.jsx +0 -80
  103. data/lib/api_maker/javascript/bootstrap/select.jsx +0 -168
  104. data/lib/api_maker/javascript/bootstrap/string-input.jsx +0 -203
  105. data/lib/api_maker/javascript/cable-connection-pool.js +0 -169
  106. data/lib/api_maker/javascript/cable-subscription-pool.js +0 -111
  107. data/lib/api_maker/javascript/cable-subscription.js +0 -33
  108. data/lib/api_maker/javascript/collection.js +0 -186
  109. data/lib/api_maker/javascript/commands-pool.js +0 -123
  110. data/lib/api_maker/javascript/custom-error.js +0 -14
  111. data/lib/api_maker/javascript/deserializer.js +0 -35
  112. data/lib/api_maker/javascript/devise.js.erb +0 -113
  113. data/lib/api_maker/javascript/error-logger.js +0 -119
  114. data/lib/api_maker/javascript/event-connection.jsx +0 -24
  115. data/lib/api_maker/javascript/event-created.jsx +0 -26
  116. data/lib/api_maker/javascript/event-destroyed.jsx +0 -26
  117. data/lib/api_maker/javascript/event-emitter-listener.jsx +0 -32
  118. data/lib/api_maker/javascript/event-listener.jsx +0 -41
  119. data/lib/api_maker/javascript/event-updated.jsx +0 -26
  120. data/lib/api_maker/javascript/form-data-to-object.js +0 -70
  121. data/lib/api_maker/javascript/included.js +0 -39
  122. data/lib/api_maker/javascript/key-value-store.js +0 -47
  123. data/lib/api_maker/javascript/logger.js +0 -23
  124. data/lib/api_maker/javascript/model-name.js +0 -21
  125. data/lib/api_maker/javascript/models-response-reader.js +0 -43
  126. data/lib/api_maker/javascript/paginate.jsx +0 -128
  127. data/lib/api_maker/javascript/params.js +0 -68
  128. data/lib/api_maker/javascript/resource-route.jsx +0 -75
  129. data/lib/api_maker/javascript/resource-routes.jsx +0 -36
  130. data/lib/api_maker/javascript/result.js +0 -25
  131. data/lib/api_maker/javascript/session-status-updater.js +0 -113
  132. data/lib/api_maker/javascript/sort-link.jsx +0 -88
  133. data/lib/api_maker/javascript/updated-attribute.jsx +0 -60
  134. data/lib/api_maker/preloader_through.rb +0 -101
  135. data/lib/api_maker/relationship_includer.rb +0 -42
@@ -1,62 +1,101 @@
1
1
  class ApiMaker::Preloader
2
- def initialize(ability: nil, args: nil, collection:, data:, include_param:, records:, select:)
2
+ attr_reader :api_maker_args, :key_path, :locals, :model_class, :preload_param
3
+
4
+ def initialize(
5
+ ability: nil,
6
+ api_maker_args: nil,
7
+ collection:,
8
+ data:,
9
+ key_path: [],
10
+ locals:,
11
+ preload_param:,
12
+ model_class: nil,
13
+ records:,
14
+ select:,
15
+ select_columns:
16
+ )
3
17
  @ability = ability
4
- @args = args
18
+ @api_maker_args = api_maker_args
5
19
  @collection = collection
6
20
  @data = data
7
- @include_param = include_param
21
+ @key_path = key_path
22
+ @locals = locals
23
+ @preload_param = preload_param
24
+ @model_class = model_class || @collection.model
8
25
  @records = records
9
26
  @select = select
27
+ @select_columns = select_columns
10
28
  end
11
29
 
12
- def fill_data
13
- parsed = ApiMaker::RelationshipIncluder.parse(@include_param)
30
+ def fill_data # rubocop:disable Metrics/AbcSize
31
+ parsed = ApiMaker::RelationshipPreloader.parse(preload_param)
14
32
  return unless parsed
15
33
 
16
34
  parsed.each do |key, value|
17
35
  next unless key
18
36
 
19
- reflection = @collection.model.reflections[key]
37
+ key_path << key
38
+
39
+ reflection = model_class.reflections[key]
20
40
  raise "Unknown reflection: #{@collection.model.name}##{key}" unless reflection
21
41
 
22
42
  fill_empty_relationships_for_key(reflection, key)
23
43
  preload_class = preload_class_for_key(reflection)
24
44
 
25
- preload_result = ApiMaker::Configuration.profile("Preloading #{reflection.klass.name} with #{preload_class.name}") do
26
- preload_class.new(
45
+ Rails.logger.debug { "API maker: Preloading #{model_class}: #{key_path.join(".")}" }
46
+
47
+ preload_result = ApiMaker::Configuration.profile(-> { "Preloading #{reflection.klass.name} with #{preload_class.name}" }) do
48
+ preload_class
49
+ .new(
50
+ ability: @ability,
51
+ api_maker_args: api_maker_args,
52
+ collection: @collection,
53
+ data: @data,
54
+ locals: locals,
55
+ records: @records,
56
+ reflection: reflection,
57
+ select: @select,
58
+ select_columns: @select_columns
59
+ )
60
+ .preload
61
+ end
62
+
63
+ if value.blank? || preload_result.empty?
64
+ key_path.pop
65
+ next
66
+ end
67
+
68
+ ApiMaker::Preloader
69
+ .new(
27
70
  ability: @ability,
28
- args: @args,
29
- collection: @collection,
71
+ api_maker_args: api_maker_args,
30
72
  data: @data,
31
- records: @records,
32
- reflection: reflection,
33
- select: @select
34
- ).preload
35
- end
73
+ collection: preload_result,
74
+ key_path: key_path.dup,
75
+ locals: locals,
76
+ preload_param: value,
77
+ model_class: reflection.klass,
78
+ records: @data.fetch(:preloaded),
79
+ select: @select,
80
+ select_columns: @select_columns
81
+ )
82
+ .fill_data
36
83
 
37
- next if value.blank? || preload_result.fetch(:collection).empty?
38
-
39
- ApiMaker::Preloader.new(
40
- ability: @ability,
41
- args: @args,
42
- data: @data,
43
- collection: preload_result.fetch(:collection),
44
- include_param: value,
45
- records: @data.fetch(:included),
46
- select: @select
47
- ).fill_data
84
+ key_path.pop
48
85
  end
49
86
  end
50
87
 
51
88
  private
52
89
 
90
+ # Smoke test to make sure we aren't doing any additional and unnecessary queries
91
+ def check_collection_loaded!
92
+ raise "Collection wasn't loaded?" if @collection.is_a?(ActiveRecord::Relation) && !@collection.loaded?
93
+ end
94
+
53
95
  def fill_empty_relationships_for_key(reflection, key)
54
- if @records.is_a?(Hash)
55
- collection_name = ApiMaker::MemoryStorage.current.resource_for_model(reflection.active_record).collection_name
56
- records_to_set = @records.fetch(collection_name).values
57
- else
58
- records_to_set = @records.select { |record| record.model.class == reflection.active_record }
59
- end
96
+ check_collection_loaded!
97
+ collection_name = ApiMaker::MemoryStorage.current.resource_for_model(reflection.active_record).collection_name
98
+ records_to_set = @collection.map { |model| @records.dig(collection_name, model.id) }
60
99
 
61
100
  case reflection.macro
62
101
  when :has_many
@@ -0,0 +1,108 @@
1
+ class ApiMaker::PreloaderBase
2
+ attr_reader :ability, :api_maker_args, :collection, :data, :locals, :records, :reflection, :reflection_name, :select, :select_columns
3
+
4
+ def initialize(ability:, api_maker_args:, data:, collection:, locals:, records:, reflection:, select:, select_columns:)
5
+ @ability = ability
6
+ @api_maker_args = api_maker_args
7
+ @data = data
8
+ @collection = collection
9
+ @locals = locals
10
+ @reflection = reflection
11
+ @reflection_name = @reflection.name
12
+ @records = records
13
+ @select = select
14
+ @select_columns = select_columns
15
+ end
16
+
17
+ def collection_ids
18
+ @collection_ids ||= collection.map do |collection_model|
19
+ collection_model[reflection.active_record.primary_key]
20
+ end
21
+ end
22
+
23
+ def underscore_name
24
+ @underscore_name ||= ApiMaker::MemoryStorage.current.resource_for_model(reflection.active_record).underscore_name
25
+ end
26
+
27
+ def models_with_join
28
+ @models_with_join ||= reflection.klass.find_by_sql(join_query.to_sql)
29
+ end
30
+
31
+ # ActiveRecord might have joined the relationship by a predictable alias. If so we need to use that alias
32
+ def joined_name
33
+ "#{reflection.name.to_s.pluralize}_#{reflection.klass.name.underscore.pluralize}"
34
+ end
35
+
36
+ def join_query
37
+ if initial_join_query.to_sql.include?(joined_name)
38
+ collection = join_query_with_joined_name
39
+ table_name = joined_name
40
+ else
41
+ collection = join_query_with_normal_name
42
+ end
43
+
44
+ ApiMaker::SelectColumnsOnCollection.execute!(
45
+ collection: collection,
46
+ model_class: reflection.klass,
47
+ select_attributes: select,
48
+ select_columns: select_columns,
49
+ table_name: table_name
50
+ )
51
+ end
52
+
53
+ def initial_join_query
54
+ reflection.active_record.joins(reflection.name)
55
+ end
56
+
57
+ def join_query_with_joined_name
58
+ # Since the joined table is using a different name, we don't need to double join the original table with an alias like under 'join_query_with_normal_name'
59
+ # The "WHERE id IN sub_query version looks like this: # .where(joined_name => {reflection.klass.primary_key => accessible_query})
60
+
61
+ exists_query = accessible_query
62
+ .select("1")
63
+ .where("#{accessible_query.klass.table_name}.#{accessible_query.klass.primary_key} = #{joined_name}.#{reflection.klass.primary_key}")
64
+
65
+ initial_join_query
66
+ .select(reflection.active_record.arel_table[reflection.active_record.primary_key].as("api_maker_origin_id"))
67
+ .where(reflection.active_record.primary_key => collection_ids)
68
+ .where("EXISTS (#{exists_query.to_sql})")
69
+ end
70
+
71
+ # Join a copy of the original table to be able to access previous table (by a copy) in the accessible query
72
+ # This is done to avoid "WHERE id IN sub_query" which is much slower than "WHERE EXISTS sub_query"
73
+ # The "WHERE id IN sub_query" version looks this simple line: .where(reflection.klass.table_name => {reflection.klass.primary_key => accessible_query})
74
+ def join_query_with_normal_name # rubocop:disable Metrics/AbcSize
75
+ query = initial_join_query
76
+ .select(reflection.active_record.arel_table[reflection.active_record.primary_key].as("api_maker_origin_id"))
77
+ .where(reflection.active_record.primary_key => collection_ids)
78
+
79
+ # No reason to add all the extra SQL if the ability has unconditioned read access
80
+ unless unconditioned_read_access?
81
+ exists_query = accessible_query
82
+ .select("1")
83
+ .where("#{accessible_query.klass.table_name}.#{accessible_query.klass.primary_key} = accessible_table.#{reflection.klass.primary_key}")
84
+
85
+ query = query
86
+ .joins(
87
+ "JOIN #{reflection.klass.table_name} AS accessible_table ON " \
88
+ "accessible_table.id = #{reflection.klass.table_name}.#{reflection.klass.primary_key}"
89
+ )
90
+ .where("EXISTS (#{exists_query.to_sql})")
91
+ end
92
+
93
+ query
94
+ end
95
+
96
+ def unconditioned_read_access?
97
+ relevant_rules = ability.__send__(:relevant_rules, :read, reflection.klass)
98
+ relevant_rules.each do |can_can_rule|
99
+ return true if can_can_rule.__send__(:conditions_empty?)
100
+ end
101
+
102
+ false
103
+ end
104
+
105
+ def accessible_query
106
+ reflection.klass.accessible_by(ability)
107
+ end
108
+ end
@@ -1,58 +1,59 @@
1
- class ApiMaker::PreloaderBelongsTo
2
- def initialize(ability:, args:, data:, collection:, records:, reflection:, select:)
3
- @ability = ability
4
- @args = args
5
- @data = data
6
- @collection = collection
7
- @reflection = reflection
8
- @reflection_name = @reflection.name
9
- @records = records
10
- @select = select
11
- end
12
-
1
+ class ApiMaker::PreloaderBelongsTo < ApiMaker::PreloaderBase
13
2
  def preload
14
3
  models.each do |model|
4
+ model_id = ApiMaker::PrimaryIdForModel.get(model)
5
+
15
6
  records_for_model(model).each do |record|
16
- record.relationships[@reflection_name] = model.id
7
+ record.relationships[reflection_name] = model_id
17
8
  end
18
9
 
19
- serializer = ApiMaker::Serializer.new(ability: @ability, args: @args, model: model, select: @select&.dig(model.class))
20
- collection_name = serializer.resource.collection_name
10
+ serializer = ApiMaker::Serializer.new(ability: ability, api_maker_args: api_maker_args, locals: locals, model: model, select: select&.dig(model.class))
11
+ underscore_name = serializer.resource.underscore_name
21
12
 
22
- @data.fetch(:included)[collection_name] ||= {}
23
- @data.fetch(:included).fetch(collection_name)[model.id] ||= serializer
13
+ data.fetch(:preloaded)[underscore_name] ||= {}
14
+ data.fetch(:preloaded).fetch(underscore_name)[model_id] ||= serializer
24
15
  end
25
16
 
26
- {collection: models}
17
+ models
27
18
  end
28
19
 
29
20
  private
30
21
 
31
- def collection_name
32
- @collection_name = ApiMaker::MemoryStorage.current.resource_for_model(@reflection.active_record).collection_name
33
- end
34
-
35
- def model_class
36
- @model_class ||= @reflection.klass
22
+ # Collects all the parent foreign keys like "users.organization_id" when preloading organizations and removes the blank (if a user doesn't have an org.)
23
+ def look_up_values
24
+ @look_up_values ||= collection.map(&reflection.foreign_key.to_sym).reject(&:blank?)
37
25
  end
38
26
 
39
27
  def models
40
- @models ||= begin
41
- models = @reflection.klass.where(look_up_key => @collection.map(&@reflection.foreign_key.to_sym).uniq)
42
- models = models.accessible_by(@ability) if @ability
43
- models.load
44
- models
28
+ @models ||= if look_up_values.empty?
29
+ # There is nothing to preload
30
+ []
31
+ else
32
+ query = reflection.klass.where(look_up_key => look_up_values)
33
+ query = query.instance_eval(&reflection.scope) if reflection.scope
34
+ query = query.accessible_by(ability) if ability
35
+ query = ApiMaker::SelectColumnsOnCollection.execute!(
36
+ collection: query,
37
+ model_class: reflection.klass,
38
+ select_attributes: select,
39
+ select_columns: select_columns,
40
+ table_name: query.klass.table_name
41
+ )
42
+
43
+ query.load
44
+ query
45
45
  end
46
46
  end
47
47
 
48
48
  def look_up_key
49
- @look_up_key ||= @reflection.options[:primary_key] || @reflection.klass.primary_key
49
+ @look_up_key ||= reflection.options[:primary_key] || reflection.klass.primary_key
50
50
  end
51
51
 
52
52
  def records_for_model(model)
53
- @records
54
- .fetch(collection_name)
53
+ # Force to string if one column is an integer and another is a string
54
+ records
55
+ .fetch(underscore_name)
55
56
  .values
56
- .select { |record| record.model.read_attribute(@reflection.foreign_key) == model.read_attribute(look_up_key) }
57
+ .select { |record| record.model[reflection.foreign_key].to_s == model[look_up_key].to_s }
57
58
  end
58
59
  end
@@ -1,68 +1,74 @@
1
- class ApiMaker::PreloaderHasMany
2
- def initialize(ability:, args:, data:, collection:, reflection:, records:, select:)
3
- @ability = ability
4
- @args = args
5
- @data = data
6
- @collection = collection
7
- @reflection = reflection
8
- @records = records
9
- @select = select
10
-
11
- raise "No inverse of for #{@reflection.active_record.name}##{@reflection.name}" unless @reflection.inverse_of
12
- end
13
-
1
+ class ApiMaker::PreloaderHasMany < ApiMaker::PreloaderBase
14
2
  def preload
15
3
  models.each do |model|
16
4
  preload_model(model)
17
5
  end
18
6
 
19
- {collection: models}
7
+ models
20
8
  end
21
9
 
22
10
  private
23
11
 
24
12
  def models
25
- @models ||= begin
26
- if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
27
- query = ApiMaker::PreloaderThrough.new(collection: @collection, reflection: @reflection).models_query_through_reflection
28
- else
29
- primary_key_column = @reflection.options[:primary_key]&.to_sym || @collection.primary_key.to_sym
30
- query = @reflection.klass.where(@reflection.foreign_key => @collection.map(&primary_key_column))
31
- query = query.joins(@reflection.inverse_of.name)
32
- end
13
+ @models ||= if use_joined_query?
14
+ models_with_join
15
+ else
16
+ primary_key_arel_column = reflection.active_record.arel_table[reflection.active_record.primary_key]
33
17
 
34
- query = query
35
- .select(@reflection.klass.arel_table[Arel.star])
36
- .select(@reflection.active_record.arel_table[@reflection.active_record.primary_key].as("api_maker_origin_id"))
37
-
38
- query = query.accessible_by(@ability) if @ability
39
- query.load
18
+ query = models_initial_query.select(primary_key_arel_column.as("api_maker_origin_id"))
19
+ query = query.instance_eval(&reflection.scope) if reflection.scope
20
+ query = query.accessible_by(ability) if ability
21
+ query = ApiMaker::SelectColumnsOnCollection.execute!(
22
+ collection: query,
23
+ model_class: reflection.klass,
24
+ select_attributes: select,
25
+ select_columns: select_columns,
26
+ table_name: query.klass.table_name
27
+ )
40
28
  query
41
29
  end
42
30
  end
43
31
 
44
- def collection_name
45
- @collection_name ||= ApiMaker::MemoryStorage.current.resource_for_model(@reflection.active_record).collection_name
32
+ def use_joined_query?
33
+ reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) || reflection.options[:as].present?
34
+ end
35
+
36
+ def models_initial_query
37
+ raise "#{reflection.active_record.name}.#{reflection.name} didn't have an `inverse_of` instruction" unless reflection.inverse_of
38
+
39
+ query = reflection.klass.where(reflection.foreign_key => collection.map(&primary_key_column))
40
+ query.joins(reflection.inverse_of.name)
46
41
  end
47
42
 
48
43
  def preload_model(model)
49
44
  origin_data = find_origin_data_for_model(model)
45
+ model_id = ApiMaker::PrimaryIdForModel.get(model)
50
46
 
51
- origin_data.fetch(:r)[@reflection.name] ||= []
52
- origin_data.fetch(:r).fetch(@reflection.name) << model.id
47
+ reflection_data = origin_data.fetch(:r)[reflection.name] ||= []
48
+ reflection_data << model_id unless reflection_data.include?(model_id)
53
49
 
54
- serializer = ApiMaker::Serializer.new(ability: @ability, args: @args, model: model, select: @select&.dig(model.class))
55
- collection_name = serializer.resource.collection_name
50
+ serializer = ApiMaker::Serializer.new(ability: ability, api_maker_args: api_maker_args, locals: locals, model: model, select: select&.dig(model.class))
51
+ underscore_name = serializer.resource.underscore_name
56
52
 
57
- @data.fetch(:included)[collection_name] ||= {}
58
- @data.fetch(:included).fetch(collection_name)[model.id] ||= serializer
53
+ data.fetch(:preloaded)[underscore_name] ||= {}
54
+ data.fetch(:preloaded).fetch(underscore_name)[model_id] ||= serializer
55
+ end
56
+
57
+ def primary_key_column
58
+ @primary_key_column ||= if reflection.options[:primary_key]
59
+ reflection.options[:primary_key]&.to_sym
60
+ elsif collection.is_a?(Array)
61
+ collection.first.class.primary_key.to_sym
62
+ else
63
+ collection.primary_key.to_sym
64
+ end
59
65
  end
60
66
 
61
67
  def find_origin_data_for_model(model)
62
- origin_id = model.read_attribute("api_maker_origin_id")
63
- origin_data = @records.fetch(collection_name).fetch(origin_id)
68
+ origin_id = model[:api_maker_origin_id]
69
+ origin_data = records.fetch(underscore_name).fetch(origin_id)
64
70
 
65
- raise "Couldn't find any origin data by that type (#{collection_name}) and ID (#{origin_id})" unless origin_data
71
+ raise "Couldn't find any origin data by that type (#{underscore_name}) and ID (#{origin_id})" unless origin_data
66
72
 
67
73
  origin_data
68
74
  end
@@ -1,70 +1,53 @@
1
- class ApiMaker::PreloaderHasOne
2
- def initialize(ability:, args:, data:, collection:, reflection:, records:, select: @select)
3
- @ability = ability
4
- @args = args
5
- @data = data
6
- @collection = collection
7
- @reflection = reflection
8
- @records = records
9
- @select = select
10
-
11
- raise "Records was nil" unless records
12
- end
13
-
14
- def collection_name
15
- @collection_name ||= ApiMaker::MemoryStorage.current.resource_for_model(@reflection.active_record).collection_name
16
- end
17
-
1
+ class ApiMaker::PreloaderHasOne < ApiMaker::PreloaderBase
18
2
  def preload
19
3
  models.each do |model|
20
- ApiMaker::Configuration.profile("Preloading #{model.class.name}##{model.id}") do
4
+ model_id = ApiMaker::PrimaryIdForModel.get(model)
5
+
6
+ ApiMaker::Configuration.profile(-> { "Preloading #{model.class.name}##{model_id}" }) do
21
7
  origin_data = origin_data_for_model(model)
22
- origin_data.fetch(:r)[@reflection.name] = model.id
8
+ origin_data.fetch(:r)[reflection.name] = model_id
23
9
 
24
- serializer = ApiMaker::Serializer.new(ability: @ability, args: @args, model: model, select: @select&.dig(model.class))
25
- collection_name = serializer.resource.collection_name
10
+ serializer = ApiMaker::Serializer.new(ability: ability, api_maker_args: api_maker_args, locals: locals, model: model, select: select&.dig(model.class))
11
+ underscore_name = serializer.resource.underscore_name
26
12
 
27
- @data.fetch(:included)[collection_name] ||= {}
28
- @data.fetch(:included).fetch(collection_name)[model.id] ||= serializer
13
+ data.fetch(:preloaded)[underscore_name] ||= {}
14
+ data.fetch(:preloaded).fetch(underscore_name)[model_id] ||= serializer
29
15
  end
30
16
  end
31
17
 
32
- {collection: models}
18
+ models
33
19
  end
34
20
 
35
21
  def models
36
- @models ||= begin
37
- if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
38
- query = query_through
39
- else
40
- query = query_normal
41
- end
42
-
43
- query = query.accessible_by(@ability) if @ability
44
- query = query.fix
22
+ @models ||= if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
23
+ models_with_join
24
+ else
25
+ query = query_normal
26
+ query = query.instance_eval(&reflection.scope) if reflection.scope
27
+ query = query.accessible_by(ability) if ability
28
+ query = ApiMaker::SelectColumnsOnCollection.execute!(
29
+ collection: query,
30
+ model_class: reflection.klass,
31
+ select_attributes: select,
32
+ select_columns: select_columns,
33
+ table_name: query.klass.table_name
34
+ )
35
+ query = query.fix if ApiMaker::DatabaseType.postgres?
45
36
  query.load
46
37
  query
47
38
  end
48
39
  end
49
40
 
50
41
  def origin_data_for_model(model)
51
- origin_id = model.read_attribute("api_maker_origin_id")
52
- @data.fetch(:included).fetch(collection_name).fetch(origin_id)
53
- end
54
-
55
- def query_through
56
- ApiMaker::PreloaderThrough.new(collection: @collection, reflection: @reflection).models_query_through_reflection
57
- .select(@reflection.klass.arel_table[Arel.star])
58
- .select(@reflection.active_record.arel_table[@reflection.active_record.primary_key].as("api_maker_origin_id"))
42
+ origin_id = model[:api_maker_origin_id]
43
+ data.dig!(:preloaded, underscore_name, origin_id)
59
44
  end
60
45
 
61
46
  def query_normal
62
- @reflection.klass.where(@reflection.foreign_key => @collection.map(&:id))
63
- .select(@reflection.klass.arel_table[Arel.star])
64
- .select(@reflection.klass.arel_table[@reflection.foreign_key].as("api_maker_origin_id"))
65
- end
47
+ query = reflection.klass.where(reflection.foreign_key => collection.map(&:id))
48
+ .select(reflection.klass.arel_table[reflection.foreign_key].as("api_maker_origin_id"))
66
49
 
67
- def resource
68
- @resource ||= ApiMaker::MemoryStorage.current.resource_for_model(@reflection.klass)
50
+ query = query.where("#{reflection.options.fetch(:as)}_type" => reflection.active_record.name) if reflection.options[:as]
51
+ query
69
52
  end
70
53
  end
@@ -1,14 +1,6 @@
1
1
  class ApiMaker::Railtie < Rails::Railtie
2
- initializer "watch routes.json for changes and reload Rails routes if changed" do |app|
3
- file_path = Rails.root.join("app", "javascript", "shared", "routes.json")
4
-
5
- reloader = app.config.file_watcher.new([file_path]) do
6
- app.reload_routes!
7
- end
8
-
9
- app.reloaders << reloader
10
- app.reloader.to_run do
11
- reloader.execute_if_updated
12
- end
2
+ initializer "watch routes.json for changes and reload Rails routes if changed" do |_app|
3
+ file_path = Rails.root.join("app/javascript/shared/routes.json")
4
+ ApiMaker::RoutesFileReloader.execute!(file_paths: [file_path]) if File.exist?(file_path)
13
5
  end
14
6
  end
@@ -0,0 +1,42 @@
1
+ class ApiMaker::RelationshipPreloader
2
+ def self.parse(preload_param)
3
+ if preload_param.nil?
4
+ nil
5
+ elsif preload_param.is_a?(String)
6
+ ApiMaker::RelationshipPreloader.parse_string(preload_param)
7
+ elsif preload_param.is_a?(Array)
8
+ ApiMaker::RelationshipPreloader.parse_array(preload_param)
9
+ else
10
+ raise "Unexpected parameter given (#{preload_param.class.name}): #{preload_param}"
11
+ end
12
+ end
13
+
14
+ def self.parse_string(preload_param)
15
+ splitted = preload_param.split(".")
16
+ initial = splitted.shift
17
+ rest = splitted.join(".")
18
+ {initial => rest}
19
+ end
20
+
21
+ def self.parse_array(preload_param)
22
+ result = {}
23
+ preload_param.each do |preload_param_i|
24
+ parsed = ApiMaker::RelationshipPreloader.parse(preload_param_i)
25
+ parsed.each do |key, value|
26
+ if result.key?(key)
27
+ if result[key].is_a?(String)
28
+ result[key] = [result[key], value]
29
+ elsif result[key].is_a?(Array)
30
+ result[key] << value
31
+ else
32
+ raise "Unknown object: #{result[key].class.name}"
33
+ end
34
+ else
35
+ result[key] = value
36
+ end
37
+ end
38
+ end
39
+
40
+ result
41
+ end
42
+ end
@@ -1,8 +1,22 @@
1
1
  class ApiMaker::ResourceRouting
2
- def self.install_resource_routes(rails_routes, layout: "react", routes: nil)
3
- routes ||= JSON.parse(File.read(Rails.root.join("app", "javascript", "shared", "routes.json")))
4
- routes.fetch("routes").each do |route|
5
- rails_routes.get route.fetch("path") => "#{layout}#show", as: route.fetch("name").to_sym
2
+ def self.install_resource_routes(rails_routes, layout: "react", route_definitions:)
3
+ rails_routes.instance_variable_set(:@api_maker_installed_routes, {}) unless rails_routes.instance_variable_get(:@api_maker_installed_routes)
4
+ installed_routes = rails_routes.instance_variable_get(:@api_maker_installed_routes)
5
+
6
+ route_definitions.fetch("routes").each do |route|
7
+ route_name = route.fetch("name").to_sym
8
+ route_as = route_name
9
+ route_path = route.fetch("path")
10
+
11
+ if installed_routes.key?(route_name)
12
+ route_duplicate_count = installed_routes.fetch(route_name)
13
+ route_as = "#{route_as}_duplicate_#{route_duplicate_count}"
14
+ end
15
+
16
+ rails_routes.get route_path => "#{layout}#show", as: route_as
17
+
18
+ installed_routes[route_name] ||= 0
19
+ installed_routes[route_name] += 1
6
20
  end
7
21
  end
8
22
  end