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,42 +1,26 @@
1
1
  class ApiMaker::UpdateCommand < ApiMaker::BaseCommand
2
- attr_reader :command, :model, :params, :serializer
2
+ attr_reader :serializer
3
3
 
4
4
  def execute!
5
- each_command do |command|
6
- @command = command
7
- @model = command.model
8
- @params = command.args || {}
5
+ ApiMaker::Configuration.profile(-> { "UpdateCommand: #{model_class.name}" }) do
9
6
  @serializer = serialized_resource(model)
7
+ sanitized_parameters = sanitize_parameters
10
8
 
11
- if command.model.update(sanitize_parameters)
9
+ if command.model.update(sanitized_parameters)
12
10
  success_response
13
11
  else
14
- failure_response
12
+ failure_save_response(model: model, params: sanitized_parameters)
15
13
  end
16
14
  end
17
-
18
- ServicePattern::Response.new(success: true)
19
- end
20
-
21
- def failure_response
22
- command.fail(
23
- model: serializer.result,
24
- success: false,
25
- errors: model.errors.full_messages
26
- )
27
15
  end
28
16
 
29
17
  def sanitize_parameters
30
- serializer.resource_instance.permitted_params(ApiMaker::PermittedParamsArgument.new(command: command, model: model))
31
- end
32
-
33
- def serialized_resource(model)
34
- ApiMaker::Serializer.new(ability: current_ability, args: api_maker_args, model: model)
18
+ serializer.resource_instance.permitted_params(ApiMaker::PermittedParamsArgument.new(command: self, model: model))
35
19
  end
36
20
 
37
21
  def success_response
38
- command.result(
39
- model: serializer.result,
22
+ succeed!(
23
+ model: serialized_model(model),
40
24
  success: true
41
25
  )
42
26
  end
@@ -1,14 +1,14 @@
1
1
  class ApiMaker::UpdateCommandService < ApiMaker::CommandService
2
- def execute
2
+ def perform
3
3
  ApiMaker::UpdateCommand.execute_in_thread!(
4
4
  ability: ability,
5
- args: args,
5
+ api_maker_args: api_maker_args,
6
6
  collection: collection,
7
7
  commands: commands,
8
8
  command_response: command_response,
9
9
  controller: controller
10
10
  )
11
- ServicePattern::Response.new(success: true)
11
+ succeed!
12
12
  end
13
13
 
14
14
  def collection
@@ -1,11 +1,8 @@
1
1
  class ApiMaker::ValidCommand < ApiMaker::BaseCommand
2
- attr_reader :command, :model, :params, :serializer
2
+ attr_reader :serializer
3
3
 
4
4
  def execute!
5
- each_command do |command|
6
- @command = command
7
- @params = command.args || {}
8
-
5
+ ApiMaker::Configuration.profile(-> { "ValidCommand: #{model_class.name}" }) do
9
6
  if command.model_id.present?
10
7
  model = resource_instance_class.find(command.model_id)
11
8
  else
@@ -15,10 +12,8 @@ class ApiMaker::ValidCommand < ApiMaker::BaseCommand
15
12
  serializer = serialized_resource(model)
16
13
  model.assign_attributes(sanitize_parameters(serializer))
17
14
 
18
- command.result(valid: model.valid?, errors: model.errors.full_messages)
15
+ succeed!(valid: model.valid?, errors: model.errors.full_messages)
19
16
  end
20
-
21
- ServicePattern::Response.new(success: true)
22
17
  end
23
18
 
24
19
  def resource_instance_class
@@ -26,10 +21,6 @@ class ApiMaker::ValidCommand < ApiMaker::BaseCommand
26
21
  end
27
22
 
28
23
  def sanitize_parameters(serializer)
29
- serializer.resource_instance.permitted_params(ApiMaker::PermittedParamsArgument.new(command: command, model: serializer.model))
30
- end
31
-
32
- def serialized_resource(model)
33
- ApiMaker::Serializer.new(ability: current_ability, args: api_maker_args, model: model)
24
+ serializer.resource_instance.permitted_params(ApiMaker::PermittedParamsArgument.new(command: self, model: serializer.model))
34
25
  end
35
26
  end
@@ -1,14 +1,14 @@
1
1
  class ApiMaker::ValidCommandService < ApiMaker::CommandService
2
- def execute
2
+ def perform
3
3
  ApiMaker::ValidCommand.execute_in_thread!(
4
4
  ability: ability,
5
- args: args,
5
+ api_maker_args: api_maker_args,
6
6
  collection: collection,
7
7
  commands: commands,
8
8
  command_response: command_response,
9
9
  controller: controller
10
10
  )
11
- ServicePattern::Response.new(success: true)
11
+ succeed!
12
12
  end
13
13
 
14
14
  def collection
@@ -0,0 +1,146 @@
1
+ class ApiMaker::ValidationErrorsGeneratorService < ApiMaker::ApplicationService
2
+ attr_reader :model, :params, :result
3
+
4
+ def initialize(model:, params:)
5
+ @model = model
6
+ @params = params
7
+ @result = []
8
+ end
9
+
10
+ def perform
11
+ path = [model.model_name.singular]
12
+
13
+ inspect_model(model, path)
14
+ inspect_params(model, params, path)
15
+ ServicePattern::Response.new(result: result)
16
+ end
17
+
18
+ def inspect_model(model, path)
19
+ return if model.errors.empty?
20
+
21
+ model.errors.details.each do |attribute_name, _errors|
22
+ attribute_type = attribute_type(model, attribute_name)
23
+ next unless attribute_type
24
+
25
+ attribute_path = path + [attribute_name]
26
+ input_name = path_to_attribute_name(attribute_path)
27
+
28
+ error_data = {
29
+ attribute_name: attribute_name,
30
+ attribute_type: attribute_type,
31
+ id: model.id,
32
+ model_name: model.model_name.param_key,
33
+ error_messages: model.errors.messages.fetch(attribute_name).to_a,
34
+ error_types: model.errors.details.fetch(attribute_name).map do |error|
35
+ error = error.fetch(:error)
36
+
37
+ if error.is_a?(Symbol)
38
+ error
39
+ else
40
+ :custom_error
41
+ end
42
+ end
43
+ }
44
+
45
+ error_data[:input_name] = input_name unless attribute_type == :base
46
+
47
+ result << error_data
48
+ end
49
+ end
50
+
51
+ def attribute_type(model, attribute_name)
52
+ if model.attribute_names.include?(attribute_name.to_s)
53
+ :attribute
54
+ elsif model.class.const_defined?(:ADDITIONAL_ATTRIBUTES_FOR_VALIDATION_ERRORS) &&
55
+ model.class.const_get(:ADDITIONAL_ATTRIBUTES_FOR_VALIDATION_ERRORS).include?(attribute_name)
56
+ :additional_attribute_for_validation
57
+ elsif model._reflections.key?(attribute_name.to_s)
58
+ :reflection
59
+ elsif model.class.try(:monetized_attributes)&.include?(attribute_name.to_s)
60
+ :monetized_attribute
61
+ elsif attribute_name == :base
62
+ :base
63
+ end
64
+ end
65
+
66
+ def error_type(attribute_type, error)
67
+ if attribute_type == :base
68
+ :base
69
+ else
70
+ error.fetch(:error)
71
+ end
72
+ end
73
+
74
+ def inspect_params(model, params, path)
75
+ params.each do |attribute_name, attribute_value|
76
+ match = attribute_name.match(/\A(.+)_attributes\Z/)
77
+ next unless match
78
+
79
+ association_name = match[1].to_sym
80
+ association = model.association(association_name)
81
+
82
+ path << attribute_name
83
+
84
+ if attribute_value.is_a?(Array)
85
+ check_nested_many_models_for_validation_errors_on_array(association.target, attribute_value, path)
86
+ elsif all_keys_numeric?(attribute_value)
87
+ # This is a has-many relationship where keys are mapped to attributes
88
+ check_nested_many_models_for_validation_errors(association.target, attribute_value, path)
89
+ else
90
+ inspect_model(association.target, path)
91
+ inspect_params(association.target, attribute_value, path)
92
+ end
93
+
94
+ path.pop
95
+ end
96
+ end
97
+
98
+ def all_keys_numeric?(hash)
99
+ hash.keys.all? { |key| key.to_s.match?(/\A\d+\Z/) }
100
+ end
101
+
102
+ def check_nested_many_models_for_validation_errors(models_up_next, attribute_value, path)
103
+ if models_up_next.length != attribute_value.keys.length
104
+ raise "Expected same length on targets and attribute values: #{models_up_next.length}, #{attribute_value.keys.length}"
105
+ end
106
+
107
+ count = 0
108
+ attribute_value.each do |unique_key, model_attribute_values|
109
+ model_up_next = models_up_next.fetch(count)
110
+ count += 1
111
+
112
+ path << unique_key
113
+ inspect_model(model_up_next, path)
114
+ inspect_params(model_up_next, model_attribute_values, path)
115
+ path.pop
116
+ end
117
+ end
118
+
119
+ def check_nested_many_models_for_validation_errors_on_array(models_up_next, attribute_value, path)
120
+ if models_up_next.length != attribute_value.length
121
+ raise "Expected same length on targets and attribute values: #{models_up_next.length}, #{attribute_value.length}"
122
+ end
123
+
124
+ count = 0
125
+ attribute_value.each_with_index do |model_attribute_values, unique_key|
126
+ model_up_next = models_up_next.fetch(count)
127
+ count += 1
128
+
129
+ path << unique_key
130
+ inspect_model(model_up_next, path)
131
+ inspect_params(model_up_next, model_attribute_values, path)
132
+ path.pop
133
+ end
134
+ end
135
+
136
+ def path_to_attribute_name(original_attribute_path)
137
+ attribute_path = original_attribute_path.dup
138
+ path_string = attribute_path.shift.dup
139
+
140
+ attribute_path.each do |path_part|
141
+ path_string << "[#{path_part}]"
142
+ end
143
+
144
+ path_string
145
+ end
146
+ end
@@ -1,15 +1,21 @@
1
- <div class="api-maker-data"<%
1
+ <script type="text/javascript">
2
+ if (!window.apiMakerDeviseCurrent) {
3
+ window.apiMakerDeviseCurrent = {}
4
+ }
2
5
 
3
- Devise.mappings.each do |scope|
4
- model = __send__("current_#{scope[0]}")
5
- next unless model
6
+ <%
7
+ Devise.mappings.each do |scope|
8
+ model = __send__("current_#{scope[0]}")
9
+ next unless model
6
10
 
7
- resource_class = ApiMaker::Serializer.resource_for(model.class)
8
- next unless resource_class
11
+ resource_class = ApiMaker::Serializer.resource_for(model.class)
12
+ next unless resource_class
9
13
 
10
- serializer = ApiMaker::Serializer.new(ability: current_ability, args: api_maker_args, model: model) if model
14
+ serializer = ApiMaker::Serializer.new(ability: current_ability, api_maker_args: api_maker_args, model: model) if model
11
15
 
12
- %> data-current-<%= scope[0].to_s.dasherize %>="<%= model ? serializer.to_json : null %>"<%
13
- end
14
-
15
- %>></div>
16
+ %>
17
+ window.apiMakerDeviseCurrent["<%= scope[0] %>"] = <%= model ? serializer.to_json(result_parser: true).html_safe : null %>
18
+ <%
19
+ end
20
+ %>
21
+ </script>
data/config/routes.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  ApiMaker::Engine.routes.draw do
2
2
  post "commands" => "commands#create"
3
- post "devise/do_sign_in" => "devise#do_sign_in"
4
- post "devise/do_sign_out" => "devise#do_sign_out"
5
3
 
6
4
  resources :session_statuses, only: :create
7
5
  end
@@ -3,9 +3,10 @@ class ApiMaker::Ability
3
3
 
4
4
  attr_reader :loader
5
5
 
6
- def initialize(args: nil)
7
- @args = args
8
- @loader = ApiMaker::AbilityLoader.new(ability: self, args: args)
6
+ def initialize(api_maker_args: nil, locals: nil)
7
+ @api_maker_args = api_maker_args || {}
8
+ @locals = locals || api_maker_args&.dig(:locals) || {}
9
+ @loader = ApiMaker::AbilityLoader.new(ability: self, locals: locals, api_maker_args: api_maker_args)
9
10
  end
10
11
 
11
12
  # Override methods from CanCan::Ability to first load abilities from the given resource
@@ -13,6 +14,16 @@ class ApiMaker::Ability
13
14
  subject = args.second
14
15
  load_abilities(subject)
15
16
  super
17
+ rescue ActiveModel::MissingAttributeError => e
18
+ if subject.is_a?(ActiveRecord::Base)
19
+ # Add subject / model class name to the error message
20
+ new_error = ActiveModel::MissingAttributeError.new("Error on #{subject.class.name}: #{e.message}")
21
+ new_error.set_backtrace(e.backtrace)
22
+
23
+ raise new_error
24
+ end
25
+
26
+ raise e
16
27
  end
17
28
 
18
29
  def model_adapter(*args)
@@ -24,15 +35,19 @@ class ApiMaker::Ability
24
35
  def load_abilities(subject)
25
36
  return unless active_record?(subject)
26
37
 
27
- if subject.class == Class
28
- loader.load_model_class(subject)
38
+ if subject.class == Class # rubocop:disable Style/ClassEqualityComparison
39
+ ApiMaker::Configuration.profile(-> { "Loading abilities for #{subject.name}" }) do
40
+ loader.load_model_class(subject)
41
+ end
29
42
  elsif subject.class != Class
30
- loader.load_model_class(subject.class)
43
+ ApiMaker::Configuration.profile(-> { "Loading abilities for #{subject.class.name}" }) do
44
+ loader.load_model_class(subject.class)
45
+ end
31
46
  end
32
47
  end
33
48
 
34
49
  def active_record?(subject)
35
- return subject < ActiveRecord::Base if subject.class == Class
50
+ return subject < ActiveRecord::Base if subject.class == Class # rubocop:disable Style/ClassEqualityComparison
36
51
 
37
52
  subject.is_a?(ActiveRecord::Base)
38
53
  end
@@ -1,21 +1,24 @@
1
1
  class ApiMaker::AbilityLoader
2
- def initialize(ability:, args:)
2
+ attr_reader :ability, :api_maker_args, :loaded, :locals, :loaded_model_names
3
+
4
+ def initialize(ability:, api_maker_args:, locals:)
3
5
  @ability = ability
4
- @args = args
6
+ @api_maker_args = api_maker_args
7
+ @locals = locals
5
8
  @loaded_model_names = {}
6
9
  end
7
10
 
8
11
  def load_model_class(model_class)
9
- return if @loaded_model_names.key?(model_class.name)
12
+ return if loaded_model_names.key?(model_class.name)
10
13
 
11
14
  resource = ApiMaker::MemoryStorage.current.resource_for_model(model_class)
12
15
  load_resource(resource)
13
16
  end
14
17
 
15
18
  def load_resource(resource)
16
- return if @loaded_model_names.key?(resource.model_class_name)
19
+ return if loaded_model_names.key?(resource.model_class_name)
17
20
 
18
- resource.new(ability: @ability, args: @args, model: nil).abilities
19
- @loaded_model_names[resource.model_class_name] = true
21
+ resource.new(ability: ability, api_maker_args: api_maker_args, locals: locals, model: nil).abilities
22
+ loaded_model_names[resource.model_class_name] = true
20
23
  end
21
24
  end
@@ -0,0 +1,15 @@
1
+ class ApiMaker::BaseCollectionInstance
2
+ ApiMaker::IncludeHelpers.execute!(klass: self)
3
+
4
+ attr_accessor :collection
5
+ attr_reader :api_maker_args, :commands, :command_response, :controller, :current_ability
6
+
7
+ def initialize(ability:, api_maker_args:, collection:, commands:, command_response:, controller:)
8
+ @api_maker_args = api_maker_args
9
+ @current_ability = ability
10
+ @collection = collection
11
+ @commands = commands
12
+ @command_response = command_response
13
+ @controller = controller
14
+ end
15
+ end
@@ -1,13 +1,23 @@
1
1
  class ApiMaker::BaseResource
2
- attr_reader :ability, :args, :model
2
+ ApiMaker::IncludeHelpers.execute!(klass: self)
3
3
 
4
- delegate :can, to: :ability
4
+ attr_reader :ability, :api_maker_args, :locals, :model
5
5
 
6
- CRUD = [:create, :read, :update, :destroy].freeze
6
+ delegate :can, :can?, allow_nil: true, to: :ability
7
+
8
+ CRUD = [:create, :create_events, :read, :update, :update_events, :destroy, :destroy_events].freeze
9
+ READ = [:create_events, :destroy_events, :read, :update_events].freeze
10
+
11
+ def self.attribute(attribute_name, **args)
12
+ # Automatically add a columns argument if the attribute name matches a column name on the models table
13
+ args[:requires_columns] = [attribute_name] if !args.key?(:requires_columns) && column_exists_on_model?(model_class, attribute_name)
14
+
15
+ ApiMaker::MemoryStorage.current.add(self, :attributes, attribute_name, args)
16
+ end
7
17
 
8
18
  def self.attributes(*attributes, **args)
9
- attributes.each do |attribute|
10
- ApiMaker::MemoryStorage.current.add(self, :attributes, attribute, args)
19
+ attributes.each do |attribute_name|
20
+ attribute(attribute_name, args)
11
21
  end
12
22
  end
13
23
 
@@ -15,12 +25,29 @@ class ApiMaker::BaseResource
15
25
  ApiMaker::MemoryStorage.current.storage_for(self, :attributes)
16
26
  end
17
27
 
28
+ def self._attributes_with_string_keys
29
+ @_attributes_with_string_keys ||= begin
30
+ result = {}
31
+ _attributes.each do |key, value|
32
+ result[key.to_s] = value
33
+ end
34
+ result
35
+ end
36
+ end
37
+
18
38
  def self.collection_commands(*list)
19
39
  list.each do |collection_command|
20
40
  ApiMaker::MemoryStorage.current.add(self, :collection_commands, collection_command)
21
41
  end
22
42
  end
23
43
 
44
+ def self.column_exists_on_model?(model_class, column_name)
45
+ model_class.column_names.include?(column_name.to_s)
46
+ rescue ActiveRecord::StatementInvalid
47
+ # This happens if the table or column doesn't exist - like if we are running during a migration
48
+ false
49
+ end
50
+
24
51
  def self.member_commands(*list)
25
52
  list.each do |member_command|
26
53
  ApiMaker::MemoryStorage.current.add(self, :member_commands, member_command)
@@ -39,7 +66,7 @@ class ApiMaker::BaseResource
39
66
  end
40
67
 
41
68
  def self.model_class_name
42
- @model_class_name ||= name.gsub(/Resource$/, "").gsub(/^Resources::/, "")
69
+ @model_class_name ||= short_name
43
70
  end
44
71
 
45
72
  def self.relationships(*relationships)
@@ -53,7 +80,7 @@ class ApiMaker::BaseResource
53
80
  end
54
81
 
55
82
  def self.collection_name
56
- @collection_name ||= plural_name.underscore.dasherize
83
+ @collection_name ||= plural_name.underscore
57
84
  end
58
85
 
59
86
  def self.default_select
@@ -66,13 +93,112 @@ class ApiMaker::BaseResource
66
93
  @plural_name ||= short_name.pluralize
67
94
  end
68
95
 
96
+ def self.require_name
97
+ @require_name ||= collection_name.singularize
98
+ end
99
+
69
100
  def self.short_name
70
101
  @short_name ||= name.match(/\AResources::(.+)Resource\Z/)[1]
71
102
  end
72
103
 
73
- def initialize(ability: nil, args: {}, model:)
104
+ def self.underscore_name
105
+ @underscore_name ||= plural_name.underscore
106
+ end
107
+
108
+ def initialize(ability: nil, api_maker_args: {}, locals:, model:)
74
109
  @ability = ability
75
- @args = args
110
+ @api_maker_args = api_maker_args
111
+ @locals = locals || api_maker_args&.dig(:locals) || {}
76
112
  @model = model
77
113
  end
114
+
115
+ def can_access_through(ability:, relationship:)
116
+ reflection = model_class.reflections.fetch(relationship.to_s)
117
+ target_model_class = reflection.klass
118
+ self.ability.load_abilities(target_model_class)
119
+ relevant_rules = self.ability.__send__(:relevant_rules, ability, target_model_class)
120
+
121
+ relevant_rules.each do |relevant_rule|
122
+ if relevant_rule.conditions.empty?
123
+ handle_empty_conditions(
124
+ model_class: model_class,
125
+ reflection: reflection,
126
+ relationship: relationship,
127
+ target_model_class: target_model_class
128
+ )
129
+ elsif relevant_rule.conditions.is_a?(Array)
130
+ handle_array_condition_rule(
131
+ ability: ability,
132
+ model_class: model_class,
133
+ reflection: reflection,
134
+ relevant_rule: relevant_rule
135
+ )
136
+ else
137
+ can ability, model_class, {
138
+ reflection.name => relevant_rule.conditions
139
+ }
140
+ end
141
+ end
142
+ end
143
+
144
+ def inspect
145
+ "#<#{self.class.name}:#{__id__}>"
146
+ end
147
+
148
+ def model_class
149
+ self.class.model_class
150
+ end
151
+
152
+ private
153
+
154
+ def handle_empty_conditions(model_class:, reflection:, relationship:, target_model_class:)
155
+ lookup_query = target_model_class
156
+ .where("#{target_model_class.table_name}.#{reflection.foreign_key} = #{model_class.table_name}.#{model_class.primary_key}")
157
+
158
+ exists_sql = "EXISTS (#{lookup_query.to_sql})"
159
+
160
+ can ability, model_class, [exists_sql] do |model|
161
+ model.__send__(relationship).any?
162
+ end
163
+ end
164
+
165
+ def handle_array_condition_rule(ability:, model_class:, reflection:, relevant_rule:)
166
+ if raw_supported_macro?(reflection.macro)
167
+ nested_sql = nested_raw_sql(
168
+ model_class: model_class,
169
+ relevant_rule: relevant_rule,
170
+ reflection: reflection
171
+ )
172
+
173
+ can ability, model_class, [nested_sql] do |model|
174
+ model_class.where(nested_sql).exists?(id: model.id)
175
+ end
176
+ else
177
+ raise "No support for macro: #{reflection.macro}"
178
+ end
179
+ end
180
+
181
+ def raw_supported_macro?(macro)
182
+ macro == :belongs_to || macro == :has_many || macro == :has_one
183
+ end
184
+
185
+ def nested_raw_sql(model_class:, reflection:, relevant_rule:)
186
+ # The conditions are given as raw SQL so we nest the original sub-query under a new one that filters on the ID of the current table as well
187
+ relationship_sql = relevant_rule.conditions.first
188
+ "EXISTS (" \
189
+ "SELECT 1 " \
190
+ "FROM #{reflection.klass.table_name} " \
191
+ "WHERE " \
192
+ "#{nested_raw_sql_condition(model_class: model_class, reflection: reflection)} AND " \
193
+ "(#{relationship_sql})" \
194
+ ")"
195
+ end
196
+
197
+ def nested_raw_sql_condition(model_class:, reflection:)
198
+ if reflection.macro == :belongs_to
199
+ "#{reflection.klass.table_name}.#{reflection.join_primary_key} = #{model_class.table_name}.#{reflection.foreign_key}"
200
+ else
201
+ "#{reflection.klass.table_name}.#{reflection.foreign_key} = #{model_class.table_name}.#{model_class.primary_key}"
202
+ end
203
+ end
78
204
  end
@@ -0,0 +1,14 @@
1
+ class ApiMaker::BaseService < ServicePattern::Service
2
+ ApiMaker::IncludeHelpers.execute!(klass: self)
3
+
4
+ attr_reader :args, :api_maker_args, :controller, :current_ability
5
+
6
+ delegate :request, allow_nil: true, to: :controller
7
+
8
+ def initialize(ability: nil, args: {}, api_maker_args: {}, controller: nil)
9
+ @args = args
10
+ @api_maker_args = api_maker_args
11
+ @controller = controller
12
+ @current_ability = ability
13
+ end
14
+ end