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