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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d65964fbfeb5bd8a5d89702083930f77e3d9c46956695c4f0877039a43c3010
4
- data.tar.gz: 2a9431af5f0bfebcfb9605d85fafc40b824448aeac9ccd5a0f595b8e9f6d642d
3
+ metadata.gz: 696d6f1f8b6baa82e326b7d05f4e6f6fdd57de5a7ad9a20d7961d1d7ec361e78
4
+ data.tar.gz: 8960e2876a0c29d700c9c429e0153f43ea31e0839a03d423ffeeb92863f18ee2
5
5
  SHA512:
6
- metadata.gz: bf2858ef950edf1aa8cc9ce45938ebd1b5ea5f3dd1f11223fd42959bff910a5cceae21599abbfdce7dbabedcdcd481c54c832c77712417436d02f423285fb48d
7
- data.tar.gz: 7f63b82811ef1a5207d2ecbdcc62072b4d4297263884de1c4b36704e7b114a2d027cbbb3dac324de69af6c19fc79f185f273b4bf070def5f7c9282e1b1caa404
6
+ metadata.gz: c185f283cb899f81293df2e93e91c9298a4c1bfe5abbc5eb56f4fd23444ff46ce42776718cc2547e103ff511e30406fdb62761363813c68d9e67375cbcee2363
7
+ data.tar.gz: 40be89f333195778c4c7455d4f76ec85a1786637346e75f91dd043296870d79f340b53193bf28420962c6f7f418858697e4daa0e803684d29af20ebb6481076a
@@ -0,0 +1,5 @@
1
+ module ApiHelpers::ApiMakerHelpers
2
+ def locals
3
+ @locals ||= ApiMaker::LocalsFromController.execute!(controller: controller)
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ class Services::CanCan::LoadAbilities < ApiMaker::BaseService
2
+ def perform
3
+ result = []
4
+
5
+ request.each do |ability_data|
6
+ # Sometimes Rails passes a hash instead of an array
7
+ ability_data = ability_data.fetch(1) if ability_data.is_a?(Array)
8
+
9
+ ability = ability_data.fetch("ability")
10
+ subject = ability_data.fetch("subject")
11
+ subject_to_check = subject
12
+
13
+ # Convert subject to original model class if resource is given
14
+ subject_to_check = subject.model_class if subject.is_a?(Class) && subject < ApiMaker::BaseResource
15
+
16
+ can = current_ability.can?(ability.to_sym, subject_to_check)
17
+ result << {
18
+ ability: ability,
19
+ can: can,
20
+ subject: subject
21
+ }
22
+ end
23
+
24
+ succeed!(abilities: result)
25
+ end
26
+
27
+ def request
28
+ @request ||= args.fetch(:request)
29
+ end
30
+ end
@@ -0,0 +1,64 @@
1
+ class Services::Devise::SignIn < ApiMaker::BaseService
2
+ include Devise::Controllers::Rememberable
3
+
4
+ # Rememberable needs this
5
+ def cookies
6
+ controller.__send__(:cookies)
7
+ end
8
+
9
+ def perform
10
+ fail! "Devise sign in isn't enabled", type: :devise_sign_in_isnt_enabled unless ApiMaker::Configuration.current.devise_sign_in_enabled
11
+
12
+ check_model_exists
13
+ check_serializer_exists
14
+
15
+ if !model.active_for_authentication?
16
+ fail! inactive_message, type: :inactive
17
+ elsif model.valid_password?(args[:password])
18
+ controller.sign_in(model, scope: scope)
19
+ remember_me(model) if args.dig(:args, :rememberMe)
20
+ succeed!(model_data: serializer.result)
21
+ else
22
+ fail! invalid_error_message, type: :invalid
23
+ end
24
+ end
25
+
26
+ def check_model_exists
27
+ error_msg = I18n.t("devise.failure.not_found_in_database", authentication_keys: model_class.authentication_keys.join(", "))
28
+ fail! error_msg, type: :not_found_in_database unless model
29
+ end
30
+
31
+ def check_serializer_exists
32
+ fail! "Serializer doesn't exist for #{scope}", type: :serializer_doesnt_exist unless resource
33
+ end
34
+
35
+ def inactive_message
36
+ message = model.inactive_message
37
+ message = I18n.t("devise.failure.#{message}") if message.is_a?(Symbol)
38
+ message
39
+ end
40
+
41
+ def invalid_error_message
42
+ I18n.t("devise.failure.invalid", authentication_keys: model_class.authentication_keys.join(", "))
43
+ end
44
+
45
+ def model
46
+ @model ||= model_class.find_for_authentication(email: args[:username])
47
+ end
48
+
49
+ def model_class
50
+ @model_class ||= scope.camelize.safe_constantize
51
+ end
52
+
53
+ def scope
54
+ @scope ||= args.dig(:args, :scope).presence || "user"
55
+ end
56
+
57
+ def resource
58
+ @resource ||= ApiMaker::Serializer.resource_for(model.class)
59
+ end
60
+
61
+ def serializer
62
+ @serializer ||= ApiMaker::Serializer.new(ability: current_ability, api_maker_args: api_maker_args, model: model)
63
+ end
64
+ end
@@ -0,0 +1,9 @@
1
+ class Services::Devise::SignOut < ApiMaker::BaseService
2
+ def perform
3
+ fail! "Devise sign out isn't enabled", type: :devise_sign_out_isnt_enabled unless ApiMaker::Configuration.current.devise_sign_out_enabled
4
+ scope = args.dig(:args, :scope).presence || "user"
5
+ current_model = controller.__send__("current_#{scope}")
6
+ controller.sign_out current_model
7
+ succeed!(success: true)
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ class Services::Models::FindOrCreateBy < ApiMaker::BaseService
2
+ def perform
3
+ resource_name = args.fetch(:resource_name)
4
+ resource = "Resources::#{resource_name}Resource".safe_constantize
5
+ model_class = resource.model_class
6
+ find_or_create_by_args = args.fetch(:find_or_create_by_args)
7
+ additional_data = args[:additional_data]
8
+
9
+ model = model_class.find_or_initialize_by(find_or_create_by_args)
10
+ model.assign_attributes(additional_data) if model.new_record? && additional_data
11
+
12
+ if model.save
13
+ succeed! model: model
14
+ else
15
+ fail! errors: model.errors.full_messages
16
+ end
17
+ end
18
+ end
@@ -1,6 +1,10 @@
1
1
  class ApiMaker::SubscriptionsChannel < ApplicationCable::Channel
2
2
  def subscribed
3
3
  params[:subscription_data].each do |model_name, subscription_types|
4
+ subscription_types["model_class_events"]&.each do |event_name|
5
+ connect_model_class_event(model_name, event_name)
6
+ end
7
+
4
8
  subscription_types["events"]&.each do |event_name, model_ids|
5
9
  connect_event(model_name, model_ids, event_name)
6
10
  end
@@ -25,16 +29,24 @@ private
25
29
  model_class = model_for_resource_name(model_name)
26
30
  channel_name = model_class.api_maker_broadcast_create_channel_name
27
31
  stream_from(channel_name, coder: ActiveSupport::JSON) do |data|
32
+ ApiMaker::Configuration.current.before_create_event_callbacks.each do |callback|
33
+ callback.call(data: data)
34
+ end
35
+
28
36
  # We need to look the model up to evaluate if the user has access
29
- model = data.fetch("model_class_name").safe_constantize.accessible_by(current_ability, :create_events).find(data.fetch("model_id"))
37
+ model_class = data.fetch("mcn").safe_constantize
30
38
 
31
- # Transmit the data to JS if its allowed
39
+ Rails.logger.debug { "API maker: ConnectCreates for #{model_class.name}" }
40
+ model = model_class.accessible_by(current_ability, :create_events).find_by(model_class.primary_key => data.fetch("mi"))
41
+
42
+ # Transmit the data to JS if its found (and thereby allowed)
32
43
  transmit data if model
33
44
  end
34
45
  end
35
46
 
36
47
  def connect_destroys(model_name, model_ids)
37
48
  model_class = model_for_resource_name(model_name)
49
+ Rails.logger.debug { "API maker: ConnectDestroys for #{model_class.name}" }
38
50
  models = model_class.accessible_by(current_ability, :destroy_events).where(model_class.primary_key => model_ids)
39
51
  models.each do |model|
40
52
  channel_name = model.api_maker_broadcast_destroy_channel_name
@@ -47,6 +59,7 @@ private
47
59
  def connect_event(model_name, model_ids, event_name)
48
60
  ability_name = "event_#{event_name}".to_sym
49
61
  model_class = model_for_resource_name(model_name)
62
+ Rails.logger.debug { "API maker: ConnectEvents for #{model_class.name} #{event_name}" }
50
63
  models = model_class.accessible_by(current_ability, ability_name).where(model_class.primary_key => model_ids)
51
64
  models.each do |model|
52
65
  channel_name = model.api_maker_event_channel_name(event_name)
@@ -56,8 +69,26 @@ private
56
69
  end
57
70
  end
58
71
 
72
+ def connect_model_class_event(model_name, event_name)
73
+ ability_name = "model_class_event_#{event_name}".to_sym
74
+ model_class = model_for_resource_name(model_name)
75
+ channel_name = model_class.api_maker_model_class_event_name(event_name)
76
+
77
+ Rails.logger.debug { "API maker: ConnectModelClassEvent for #{model_class.name} #{event_name}" }
78
+
79
+ if current_ability.can?(ability_name, model_class)
80
+ stream_from(channel_name, coder: ActiveSupport::JSON) do |data|
81
+ transmit data
82
+ end
83
+ else
84
+ Rails.logger.warn { "API maker: No access to model class event #{model_class.name}##{event_name} with ability name: #{ability_name}" }
85
+ end
86
+ end
87
+
59
88
  def connect_updates(model_name, model_ids)
60
89
  model_class = model_for_resource_name(model_name)
90
+ Rails.logger.debug { "API maker: ConnectUpdates for #{model_class.name}" }
91
+
61
92
  models = model_class.accessible_by(current_ability, :update_events).where(model_class.primary_key => model_ids)
62
93
  models.each do |model|
63
94
  channel_name = model.api_maker_broadcast_update_channel_name
@@ -8,15 +8,19 @@ class ApiMaker::BaseController < ApplicationController
8
8
  private
9
9
 
10
10
  def current_ability
11
- @current_ability ||= ::ApiMaker::Ability.new(args: api_maker_args)
11
+ @current_ability ||= ApiMaker::Configuration.current.ability_class.new(api_maker_args: api_maker_args, locals: api_maker_locals)
12
+ end
13
+
14
+ def api_maker_locals
15
+ @api_maker_locals ||= {}
12
16
  end
13
17
 
14
18
  def render_error(error)
15
19
  puts error.inspect
16
- puts error.backtrace
20
+ puts Rails.backtrace_cleaner.clean(error.backtrace)
17
21
 
18
22
  logger.error error.inspect
19
- logger.error error.backtrace.join("\n")
23
+ logger.error Rails.backtrace_cleaner.clean(error.backtrace).join("\n")
20
24
 
21
25
  raise error
22
26
  end
@@ -1,19 +1,21 @@
1
1
  class ApiMaker::CommandsController < ApiMaker::BaseController
2
+ wrap_parameters false
3
+
2
4
  def create
3
5
  command_response = ApiMaker::CommandResponse.new(controller: self)
4
6
  controller = self
5
7
 
6
- params[:pool].each do |command_type, command_type_data|
8
+ merged_params.fetch(:pool).each do |command_type, command_type_data|
7
9
  command_type_data.each do |resource_plural_name, command_model_data|
8
10
  command_model_data.each do |command_name, command_data|
9
11
  ApiMaker.const_get("#{command_type.camelize}CommandService").execute!(
10
12
  ability: current_ability,
11
- args: api_maker_args,
13
+ api_maker_args: api_maker_args,
12
14
  command_response: command_response,
13
15
  commands: command_data,
14
16
  command_name: command_name,
15
- resource_name: resource_plural_name,
16
- controller: controller
17
+ controller: controller,
18
+ resource_name: resource_plural_name
17
19
  )
18
20
  end
19
21
  end
@@ -23,4 +25,24 @@ class ApiMaker::CommandsController < ApiMaker::BaseController
23
25
 
24
26
  render json: {responses: command_response.result}
25
27
  end
28
+
29
+ def json_params
30
+ @json_params ||= ActionController::Parameters.new(json_data) if json_data
31
+ end
32
+
33
+ def json_data
34
+ @json_data ||= JSON.parse(params[:json]) if params[:json]
35
+ end
36
+
37
+ def merged_params
38
+ @merged_params ||= if json_data
39
+ raw_data = params.permit!.to_h
40
+ merged_data = ApiMaker::DeepMergeParams.execute!(json_data, raw_data)
41
+ ActionController::Parameters.new(merged_data)
42
+ elsif json_params
43
+ json_params
44
+ else
45
+ params
46
+ end
47
+ end
26
48
  end
@@ -1,4 +1,4 @@
1
- class ApiMaker::SessionStatusesController < ActionController::Base
1
+ class ApiMaker::SessionStatusesController < ActionController::Base # rubocop:disable Rails/ApplicationController
2
2
  skip_before_action :verify_authenticity_token, raise: false
3
3
 
4
4
  def create
@@ -0,0 +1,104 @@
1
+ class ApiMaker::AbilitiesLoader < ApiMaker::ApplicationService
2
+ attr_reader :abilities, :abilities_data, :abilities_with_no_conditions, :abilities_with_no_rules, :ability, :groupings, :serializers
3
+
4
+ def initialize(abilities:, ability:, serializers:)
5
+ @ability = ability
6
+ @abilities = abilities.map(&:to_sym)
7
+ @abilities_data = {}
8
+ @abilities_with_no_conditions = []
9
+ @abilities_with_no_rules = []
10
+ @groupings = {}
11
+ @serializers = serializers
12
+ end
13
+
14
+ def perform
15
+ scan_abilities
16
+ scan_groupings
17
+
18
+ load_abilities_with_no_conditions if abilities_with_no_conditions.any?
19
+ load_abilities_with_no_rules if abilities_with_no_rules.any?
20
+
21
+ groupings.each_value do |ability_names|
22
+ load_abilities_with_conditions(ability_names)
23
+ end
24
+
25
+ succeed!
26
+ end
27
+
28
+ def scan_abilities
29
+ abilities.each do |ability_name|
30
+ abilities_data[ability_name] = {
31
+ combined_conditions: [],
32
+ rules_count: 0,
33
+ rule_with_no_conditions: false
34
+ }
35
+
36
+ relevant_rules = ability.__send__(:relevant_rules, ability_name, model_class)
37
+ relevant_rules.each do |can_can_rule|
38
+ abilities_data[ability_name][:combined_conditions] << can_can_rule.conditions.to_s
39
+ abilities_data[ability_name][:rules_count] += 1
40
+
41
+ if can_can_rule.__send__(:conditions_empty?)
42
+ abilities_data[ability_name][:rule_with_no_conditions] = true
43
+ abilities_with_no_conditions << ability_name
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def scan_groupings
50
+ abilities_data.each do |ability_name, ability_data|
51
+ # No reason to create a group with no conditions - we are giving access to these without doing a query elsewhere
52
+ next if ability_data.fetch(:rule_with_no_conditions)
53
+
54
+ # If no rules have been defined, then we can safely assume no access without doing a query elsewhere
55
+ if ability_data.fetch(:rules_count).zero?
56
+ abilities_with_no_rules << ability_name
57
+ else
58
+ identifier = ability_data[:combined_conditions].join("___")
59
+
60
+ groupings[identifier] ||= []
61
+ groupings[identifier] << ability_name
62
+ end
63
+ end
64
+
65
+ groupings
66
+ end
67
+
68
+ def load_abilities_with_no_conditions
69
+ Rails.logger.debug { "API maker: Loading abilities with no condition: [#{abilities_with_no_conditions.join(", ")}], #{model_class}" }
70
+ abilities_with_no_conditions.each do |ability_name|
71
+ serializers.each do |serializer|
72
+ serializer.load_ability(ability_name, true)
73
+ end
74
+ end
75
+ end
76
+
77
+ def load_abilities_with_no_rules
78
+ Rails.logger.debug { "API maker: Loading abilities with no rules: [#{abilities_with_no_rules.join(", ")}], #{model_class}" }
79
+ abilities_with_no_rules.each do |ability_name|
80
+ serializers.each do |serializer|
81
+ serializer.load_ability(ability_name, false)
82
+ end
83
+ end
84
+ end
85
+
86
+ def load_abilities_with_conditions(ability_names)
87
+ Rails.logger.debug { "API maker: Loading abilities with conditions through query: [#{ability_names.join(", ")}], #{model_class}" }
88
+
89
+ model_ids = model_class
90
+ .accessible_by(ability, ability_names.first)
91
+ .where(id: serializers.map(&:id))
92
+ .pluck(model_class.primary_key)
93
+
94
+ serializers.each do |serializer|
95
+ ability_names.each do |ability_name|
96
+ serializer.load_ability(ability_name, model_ids.include?(serializer.id))
97
+ end
98
+ end
99
+ end
100
+
101
+ def model_class
102
+ @model_class ||= serializers.first.model.class
103
+ end
104
+ end
@@ -1,7 +1,8 @@
1
1
  class ApiMaker::ApplicationService < ServicePattern::Service
2
+ # Replaces magic variables with actual variable names
2
3
  def api_maker_json(object)
3
4
  json = object.to_json
4
- json.gsub!(/"\{\{(.+?)\}\}"/, "\\1")
5
+ json.gsub!(/"\{\{([A-z_]+?)\}\}"/, "\\1")
5
6
  json
6
7
  end
7
8
  end
@@ -0,0 +1,248 @@
1
+ class ApiMaker::BaseCommand
2
+ ApiMaker::IncludeHelpers.execute!(klass: self)
3
+
4
+ attr_reader :api_maker_args, :collection, :collection_instance, :command, :commands, :command_response, :controller, :current_ability
5
+
6
+ delegate :args, :model, :model_id, to: :command
7
+ delegate :result_for_command, to: :command_response
8
+
9
+ # Returns true if the gem "goldiloader" is present in the app
10
+ def self.goldiloader?
11
+ @goldiloader = Gem::Specification.find_all_by_name("goldiloader").any? if @goldiloader.nil?
12
+ @goldiloader
13
+ end
14
+
15
+ def initialize(ability:, api_maker_args:, collection:, collection_instance:, command:, commands:, command_response:, controller:)
16
+ @api_maker_args = api_maker_args
17
+ @current_ability = ability
18
+ @collection = collection
19
+ @collection_instance = collection_instance
20
+ @command = command
21
+ @commands = commands
22
+ @command_response = command_response
23
+ @controller = controller
24
+ end
25
+
26
+ def execute_with_response
27
+ execute!
28
+ rescue ApiMaker::CommandFailedError => e
29
+ command.fail(*e.api_maker_args, &e.api_maker_block)
30
+ end
31
+
32
+ def self.command_error_message(error)
33
+ if Rails.application.config.consider_all_requests_local
34
+ "#{error.class.name}: #{error.message}"
35
+ else
36
+ "Internal server error"
37
+ end
38
+ end
39
+
40
+ def self.execute_in_thread!(ability:, api_maker_args:, collection:, commands:, command_response:, controller:)
41
+ command_response.with_thread do
42
+ if const_defined?(:CollectionInstance)
43
+ collection_instance = const_get(:CollectionInstance).new(
44
+ ability: ability,
45
+ api_maker_args: api_maker_args,
46
+ collection: collection,
47
+ commands: commands,
48
+ command_response: command_response,
49
+ controller: controller
50
+ )
51
+
52
+ collection = collection_instance.custom_collection if collection_instance.respond_to?(:custom_collection)
53
+ collection_instance.collection = collection
54
+
55
+ threadded = collection_instance.try(:threadded?)
56
+ end
57
+
58
+ if threadded
59
+ # Goldiloader doesn't work with threads (loads all relationships for each thread)
60
+ collection = collection.auto_include(false) if ApiMaker::BaseCommand.goldiloader?
61
+
62
+ # Load relationship before commands so each command doesn't query on its own
63
+ collection.load
64
+ end
65
+
66
+ each_command(collection: collection, command_response: command_response, commands: commands, controller: controller, threadded: threadded) do |command|
67
+ command_instance = new(
68
+ ability: ability,
69
+ api_maker_args: api_maker_args,
70
+ collection: collection,
71
+ collection_instance: collection_instance,
72
+ command: command,
73
+ commands: command,
74
+ command_response: command_response,
75
+ controller: controller
76
+ )
77
+ command_instance.execute_with_response
78
+ end
79
+ end
80
+ end
81
+
82
+ def self.each_command(collection:, command_response:, commands:, controller:, threadded:, &blk)
83
+ commands.each do |command_id, command_data|
84
+ if threadded
85
+ command_response.with_thread do
86
+ run_command(
87
+ command_id: command_id,
88
+ command_data: command_data,
89
+ command_response: command_response,
90
+ collection: collection,
91
+ controller: controller,
92
+ &blk
93
+ )
94
+ end
95
+ else
96
+ run_command(
97
+ command_id: command_id,
98
+ command_data: command_data,
99
+ command_response: command_response,
100
+ collection: collection,
101
+ controller: controller,
102
+ &blk
103
+ )
104
+ end
105
+ end
106
+ end
107
+
108
+ def self.run_command(collection:, command_id:, command_data:, command_response:, controller:)
109
+ command = ApiMaker::IndividualCommand.new(
110
+ args: ApiMaker::Deserializer.execute!(arg: command_data[:args]),
111
+ collection: collection,
112
+ command: self,
113
+ id: command_id,
114
+ primary_key: command_data[:primary_key],
115
+ response: command_response
116
+ )
117
+
118
+ begin
119
+ yield command
120
+ rescue => e # rubocop:disable Style/RescueStandardError
121
+ error_response = {
122
+ success: false,
123
+ errors: [{message: command_error_message(e), type: :runtime_error}]
124
+ }
125
+
126
+ Rails.logger.error e.message
127
+ Rails.logger.error Rails.backtrace_cleaner.clean(e.backtrace).join("\n")
128
+
129
+ ApiMaker::Configuration.current.report_error(
130
+ command: command,
131
+ controller: controller,
132
+ error: e,
133
+ response: error_response
134
+ )
135
+
136
+ command.error(error_response)
137
+ end
138
+ end
139
+
140
+ def execute_service_or_fail(service_class, *args, &blk)
141
+ response = service_class.execute(*args, &blk)
142
+
143
+ if response.success?
144
+ succeed!(success: true)
145
+ else
146
+ fail_command_from_service_error_response(response)
147
+ end
148
+ end
149
+
150
+ def fail_command_from_service_error_response(response)
151
+ fail!(errors: serialize_service_errors(response.errors))
152
+ end
153
+
154
+ def failure_response(errors:)
155
+ fail!(
156
+ model: serialized_model(model),
157
+ success: false,
158
+ errors: errors
159
+ )
160
+ end
161
+
162
+ def failure_save_response(additional_attributes: [], model:, params:, simple_model_errors: false)
163
+ raise "Cannot receive additional attributes unless simple model errors" if !additional_attributes.empty? && !simple_model_errors
164
+
165
+ error_messages = if simple_model_errors
166
+ ApiMaker::SimpleModelErrors.execute!(additional_attributes: additional_attributes, model: model)
167
+ else
168
+ model.errors.full_messages
169
+ end
170
+
171
+ fail!(
172
+ error_type: :validation_error,
173
+ errors: error_messages.map { |error_message| {message: error_message, type: :validation_error} },
174
+ model: serialized_model(model),
175
+ success: false,
176
+ validation_errors: ApiMaker::ValidationErrorsGeneratorService.execute!(model: model, params: params)
177
+ )
178
+ end
179
+
180
+ def model_class
181
+ @model_class ||= collection.klass
182
+ end
183
+
184
+ def save_models_or_fail(*models, simple_model_errors: false)
185
+ response = ApiMaker::Models::Save.execute(models: models, simple_model_errors: simple_model_errors)
186
+
187
+ if response.success?
188
+ succeed!(success: true)
189
+ true
190
+ else
191
+ fail!(errors: response.error_messages.map { |error| {message: error, type: :validation_error} })
192
+ end
193
+ end
194
+
195
+ def serialize_service_errors(errors)
196
+ errors.map do |error|
197
+ {
198
+ message: error.message,
199
+ type: error.type
200
+ }
201
+ end
202
+ end
203
+
204
+ def fail!(*args, &blk)
205
+ if args.is_a?(Hash) && args.key?(:errors)
206
+ error_messages = args.fetch(:errors).map do |error|
207
+ if error.is_a?(Hash) && error.key?(:message)
208
+ error.fetch(:message)
209
+ else
210
+ error
211
+ end
212
+ end
213
+ else
214
+ error_messages = ["Command failed"]
215
+ end
216
+
217
+ error = ApiMaker::CommandFailedError.new(error_messages)
218
+ error.api_maker_args = args
219
+ error.api_maker_block = blk
220
+
221
+ raise error
222
+ end
223
+
224
+ def succeed!(*args, &blk)
225
+ command.result(*args, &blk)
226
+ end
227
+
228
+ def inspect
229
+ "#<#{self.class.name}:#{__id__}>"
230
+ end
231
+
232
+ private
233
+
234
+ def serialized_model(model)
235
+ collection_serializer = ApiMaker::CollectionSerializer.new(
236
+ ability: current_ability,
237
+ api_maker_args: api_maker_args,
238
+ collection: [model],
239
+ model_class: model.class,
240
+ query_params: args&.dig(:query_params)
241
+ )
242
+ collection_serializer.result
243
+ end
244
+
245
+ def serialized_resource(model)
246
+ ApiMaker::Serializer.new(ability: current_ability, api_maker_args: api_maker_args, model: model)
247
+ end
248
+ end