api_maker 0.0.1 → 0.0.2

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 (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