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.
- checksums.yaml +4 -4
- data/app/api_maker/api_helpers/api_maker_helpers.rb +5 -0
- data/app/api_maker/services/can_can/load_abilities.rb +30 -0
- data/app/api_maker/services/devise/sign_in.rb +64 -0
- data/app/api_maker/services/devise/sign_out.rb +9 -0
- data/app/api_maker/services/models/find_or_create_by.rb +18 -0
- data/app/channels/api_maker/subscriptions_channel.rb +33 -2
- data/app/controllers/api_maker/base_controller.rb +7 -3
- data/app/controllers/api_maker/commands_controller.rb +26 -4
- data/app/controllers/api_maker/session_statuses_controller.rb +1 -1
- data/app/services/api_maker/abilities_loader.rb +104 -0
- data/app/services/api_maker/application_service.rb +2 -1
- data/app/services/api_maker/base_command.rb +248 -0
- data/app/services/api_maker/collection_command_service.rb +29 -15
- data/app/services/api_maker/collection_loader.rb +124 -0
- data/app/services/api_maker/command_failed_error.rb +3 -0
- data/app/services/api_maker/command_response.rb +17 -6
- data/app/services/api_maker/command_service.rb +3 -3
- data/app/services/api_maker/create_command.rb +11 -26
- data/app/services/api_maker/create_command_service.rb +3 -3
- data/app/services/api_maker/database_type.rb +9 -0
- data/app/services/api_maker/deep_merge_params.rb +26 -0
- data/app/services/api_maker/deserializer.rb +35 -0
- data/app/services/api_maker/destroy_command.rb +15 -21
- data/app/services/api_maker/destroy_command_service.rb +3 -3
- data/app/services/api_maker/generate_react_native_api_service.rb +3 -19
- data/app/services/api_maker/include_helpers.rb +17 -0
- data/app/services/api_maker/index_command.rb +8 -88
- data/app/services/api_maker/index_command_service.rb +5 -5
- data/app/services/api_maker/js_method_namer_service.rb +1 -1
- data/app/services/api_maker/locals_from_controller.rb +14 -0
- data/app/services/api_maker/member_command_service.rb +15 -13
- data/app/services/api_maker/model_classes_java_script_generator_service.rb +37 -0
- data/app/services/api_maker/model_content_generator_service.rb +17 -21
- data/app/services/api_maker/models/save.rb +29 -0
- data/app/services/api_maker/models_finder_service.rb +6 -2
- data/app/services/api_maker/models_generator_service.rb +6 -43
- data/app/services/api_maker/move_components_to_routes.rb +50 -0
- data/app/services/api_maker/primary_id_for_model.rb +6 -0
- data/app/services/api_maker/reset_indexed_db_service.rb +36 -0
- data/app/services/api_maker/routes_file_reloader.rb +20 -0
- data/app/services/api_maker/select_columns_on_collection.rb +78 -0
- data/app/services/api_maker/select_parser.rb +32 -0
- data/app/services/api_maker/service_command.rb +27 -0
- data/app/services/api_maker/service_command_service.rb +14 -0
- data/app/services/api_maker/simple_model_errors.rb +52 -0
- data/app/services/api_maker/update_command.rb +8 -24
- data/app/services/api_maker/update_command_service.rb +3 -3
- data/app/services/api_maker/valid_command.rb +4 -13
- data/app/services/api_maker/valid_command_service.rb +3 -3
- data/app/services/api_maker/validation_errors_generator_service.rb +146 -0
- data/app/views/api_maker/_data.html.erb +17 -11
- data/config/routes.rb +0 -2
- data/lib/api_maker/ability.rb +22 -7
- data/lib/api_maker/ability_loader.rb +9 -6
- data/lib/api_maker/base_collection_instance.rb +15 -0
- data/lib/api_maker/base_resource.rb +135 -9
- data/lib/api_maker/base_service.rb +14 -0
- data/lib/api_maker/collection_serializer.rb +95 -34
- data/lib/api_maker/command_spec_helper.rb +41 -11
- data/lib/api_maker/configuration.rb +31 -4
- data/lib/api_maker/expect_to_able_to_helper.rb +31 -0
- data/lib/api_maker/individual_command.rb +24 -9
- data/lib/api_maker/javascript/model-template.js.erb +39 -25
- data/lib/api_maker/javascript/models.js.erb +6 -0
- data/lib/api_maker/loader.rb +1 -1
- data/lib/api_maker/memory_storage.rb +1 -1
- data/lib/api_maker/model_extensions.rb +34 -18
- data/lib/api_maker/permitted_params_argument.rb +5 -1
- data/lib/api_maker/preloader.rb +71 -32
- data/lib/api_maker/preloader_base.rb +108 -0
- data/lib/api_maker/preloader_belongs_to.rb +34 -33
- data/lib/api_maker/preloader_has_many.rb +45 -39
- data/lib/api_maker/preloader_has_one.rb +30 -47
- data/lib/api_maker/railtie.rb +3 -11
- data/lib/api_maker/relationship_preloader.rb +42 -0
- data/lib/api_maker/resource_routing.rb +18 -4
- data/lib/api_maker/result_parser.rb +34 -20
- data/lib/api_maker/serializer.rb +53 -22
- data/lib/api_maker/spec_helper/browser_logs.rb +14 -0
- data/lib/api_maker/spec_helper/execute_collection_command.rb +46 -0
- data/lib/api_maker/spec_helper/execute_member_command.rb +52 -0
- data/lib/api_maker/spec_helper/expect_no_browser_errors.rb +18 -0
- data/lib/api_maker/spec_helper/wait_for_expect.rb +20 -0
- data/lib/api_maker/spec_helper/wait_for_flash_message.rb +21 -0
- data/lib/api_maker/spec_helper.rb +112 -48
- data/lib/api_maker/version.rb +1 -1
- data/lib/api_maker.rb +7 -3
- metadata +108 -89
- data/README.md +0 -476
- data/app/controllers/api_maker/devise_controller.rb +0 -60
- data/lib/api_maker/base_command.rb +0 -81
- data/lib/api_maker/javascript/api.js +0 -92
- data/lib/api_maker/javascript/base-model.js +0 -543
- data/lib/api_maker/javascript/bootstrap/attribute-row.jsx +0 -16
- data/lib/api_maker/javascript/bootstrap/attribute-rows.jsx +0 -47
- data/lib/api_maker/javascript/bootstrap/card.jsx +0 -79
- data/lib/api_maker/javascript/bootstrap/checkbox.jsx +0 -127
- data/lib/api_maker/javascript/bootstrap/checkboxes.jsx +0 -105
- data/lib/api_maker/javascript/bootstrap/live-table.jsx +0 -168
- data/lib/api_maker/javascript/bootstrap/money-input.jsx +0 -136
- data/lib/api_maker/javascript/bootstrap/radio-buttons.jsx +0 -80
- data/lib/api_maker/javascript/bootstrap/select.jsx +0 -168
- data/lib/api_maker/javascript/bootstrap/string-input.jsx +0 -203
- data/lib/api_maker/javascript/cable-connection-pool.js +0 -169
- data/lib/api_maker/javascript/cable-subscription-pool.js +0 -111
- data/lib/api_maker/javascript/cable-subscription.js +0 -33
- data/lib/api_maker/javascript/collection.js +0 -186
- data/lib/api_maker/javascript/commands-pool.js +0 -123
- data/lib/api_maker/javascript/custom-error.js +0 -14
- data/lib/api_maker/javascript/deserializer.js +0 -35
- data/lib/api_maker/javascript/devise.js.erb +0 -113
- data/lib/api_maker/javascript/error-logger.js +0 -119
- data/lib/api_maker/javascript/event-connection.jsx +0 -24
- data/lib/api_maker/javascript/event-created.jsx +0 -26
- data/lib/api_maker/javascript/event-destroyed.jsx +0 -26
- data/lib/api_maker/javascript/event-emitter-listener.jsx +0 -32
- data/lib/api_maker/javascript/event-listener.jsx +0 -41
- data/lib/api_maker/javascript/event-updated.jsx +0 -26
- data/lib/api_maker/javascript/form-data-to-object.js +0 -70
- data/lib/api_maker/javascript/included.js +0 -39
- data/lib/api_maker/javascript/key-value-store.js +0 -47
- data/lib/api_maker/javascript/logger.js +0 -23
- data/lib/api_maker/javascript/model-name.js +0 -21
- data/lib/api_maker/javascript/models-response-reader.js +0 -43
- data/lib/api_maker/javascript/paginate.jsx +0 -128
- data/lib/api_maker/javascript/params.js +0 -68
- data/lib/api_maker/javascript/resource-route.jsx +0 -75
- data/lib/api_maker/javascript/resource-routes.jsx +0 -36
- data/lib/api_maker/javascript/result.js +0 -25
- data/lib/api_maker/javascript/session-status-updater.js +0 -113
- data/lib/api_maker/javascript/sort-link.jsx +0 -88
- data/lib/api_maker/javascript/updated-attribute.jsx +0 -60
- data/lib/api_maker/preloader_through.rb +0 -101
- data/lib/api_maker/relationship_includer.rb +0 -42
@@ -1,42 +1,26 @@
|
|
1
1
|
class ApiMaker::UpdateCommand < ApiMaker::BaseCommand
|
2
|
-
attr_reader :
|
2
|
+
attr_reader :serializer
|
3
3
|
|
4
4
|
def execute!
|
5
|
-
|
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(
|
9
|
+
if command.model.update(sanitized_parameters)
|
12
10
|
success_response
|
13
11
|
else
|
14
|
-
|
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:
|
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
|
-
|
39
|
-
model:
|
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
|
2
|
+
def perform
|
3
3
|
ApiMaker::UpdateCommand.execute_in_thread!(
|
4
4
|
ability: ability,
|
5
|
-
|
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
|
-
|
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 :
|
2
|
+
attr_reader :serializer
|
3
3
|
|
4
4
|
def execute!
|
5
|
-
|
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
|
-
|
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:
|
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
|
2
|
+
def perform
|
3
3
|
ApiMaker::ValidCommand.execute_in_thread!(
|
4
4
|
ability: ability,
|
5
|
-
|
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
|
-
|
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
|
-
<
|
1
|
+
<script type="text/javascript">
|
2
|
+
if (!window.apiMakerDeviseCurrent) {
|
3
|
+
window.apiMakerDeviseCurrent = {}
|
4
|
+
}
|
2
5
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
+
<%
|
7
|
+
Devise.mappings.each do |scope|
|
8
|
+
model = __send__("current_#{scope[0]}")
|
9
|
+
next unless model
|
6
10
|
|
7
|
-
|
8
|
-
|
11
|
+
resource_class = ApiMaker::Serializer.resource_for(model.class)
|
12
|
+
next unless resource_class
|
9
13
|
|
10
|
-
|
14
|
+
serializer = ApiMaker::Serializer.new(ability: current_ability, api_maker_args: api_maker_args, model: model) if model
|
11
15
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
data/lib/api_maker/ability.rb
CHANGED
@@ -3,9 +3,10 @@ class ApiMaker::Ability
|
|
3
3
|
|
4
4
|
attr_reader :loader
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@
|
8
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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
|
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
|
19
|
+
return if loaded_model_names.key?(resource.model_class_name)
|
17
20
|
|
18
|
-
resource.new(ability:
|
19
|
-
|
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
|
-
|
2
|
+
ApiMaker::IncludeHelpers.execute!(klass: self)
|
3
3
|
|
4
|
-
|
4
|
+
attr_reader :ability, :api_maker_args, :locals, :model
|
5
5
|
|
6
|
-
|
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 |
|
10
|
-
|
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 ||=
|
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
|
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
|
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
|
-
@
|
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
|