rhino_project_core 0.20.0.alpha.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +28 -0
  4. data/Rakefile +35 -0
  5. data/app/assets/stripe_flow.png +0 -0
  6. data/app/controllers/concerns/rhino/authenticated.rb +18 -0
  7. data/app/controllers/concerns/rhino/error_handling.rb +60 -0
  8. data/app/controllers/concerns/rhino/paper_trail_whodunnit.rb +11 -0
  9. data/app/controllers/concerns/rhino/permit.rb +38 -0
  10. data/app/controllers/concerns/rhino/set_current_user.rb +13 -0
  11. data/app/controllers/rhino/account_controller.rb +34 -0
  12. data/app/controllers/rhino/active_model_extension_controller.rb +52 -0
  13. data/app/controllers/rhino/base_controller.rb +23 -0
  14. data/app/controllers/rhino/crud_controller.rb +57 -0
  15. data/app/controllers/rhino/simple_controller.rb +13 -0
  16. data/app/controllers/rhino/simple_stream_controller.rb +12 -0
  17. data/app/helpers/rhino/omniauth_helper.rb +67 -0
  18. data/app/helpers/rhino/policy_helper.rb +42 -0
  19. data/app/helpers/rhino/segment_helper.rb +62 -0
  20. data/app/models/rhino/account.rb +13 -0
  21. data/app/models/rhino/current.rb +7 -0
  22. data/app/models/rhino/user.rb +44 -0
  23. data/app/overrides/active_record/autosave_association_override.rb +18 -0
  24. data/app/overrides/active_record/delegated_type_override.rb +14 -0
  25. data/app/overrides/activestorage/direct_uploads_controller_override.rb +23 -0
  26. data/app/overrides/activestorage/redirect_controller_override.rb +21 -0
  27. data/app/overrides/activestorage/redirect_representation_controller_override.rb +21 -0
  28. data/app/overrides/devise_token_auth/confirmations_controller_override.rb +14 -0
  29. data/app/overrides/devise_token_auth/omniauth_callbacks_controller_override.rb +45 -0
  30. data/app/overrides/devise_token_auth/passwords_controller_override.rb +9 -0
  31. data/app/overrides/devise_token_auth/registrations_controller_override.rb +20 -0
  32. data/app/overrides/devise_token_auth/sessions_controller_override.rb +26 -0
  33. data/app/overrides/devise_token_auth/token_validations_controller_override.rb +18 -0
  34. data/app/policies/rhino/account_policy.rb +27 -0
  35. data/app/policies/rhino/active_storage_attachment_policy.rb +16 -0
  36. data/app/policies/rhino/admin_policy.rb +20 -0
  37. data/app/policies/rhino/base_policy.rb +72 -0
  38. data/app/policies/rhino/crud_policy.rb +109 -0
  39. data/app/policies/rhino/editor_policy.rb +12 -0
  40. data/app/policies/rhino/global_policy.rb +8 -0
  41. data/app/policies/rhino/resource_info_policy.rb +9 -0
  42. data/app/policies/rhino/user_policy.rb +20 -0
  43. data/app/policies/rhino/viewer_policy.rb +19 -0
  44. data/app/resources/rhino/info_graph.rb +41 -0
  45. data/app/resources/rhino/open_api_info.rb +108 -0
  46. data/config/routes.rb +19 -0
  47. data/db/migrate/20180101000000_devise_token_auth_create_users.rb +54 -0
  48. data/db/migrate/20180622142754_add_allow_change_password_to_users.rb +5 -0
  49. data/db/migrate/20191217010224_add_approved_to_users.rb +7 -0
  50. data/db/migrate/20200503182019_change_tokens_to_json_b.rb +9 -0
  51. data/lib/commands/rhino/module/coverage_command.rb +44 -0
  52. data/lib/commands/rhino/module/dummy_command.rb +43 -0
  53. data/lib/commands/rhino/module/new_command.rb +34 -0
  54. data/lib/commands/rhino/module/rails_command.rb +43 -0
  55. data/lib/commands/rhino/module/test_command.rb +43 -0
  56. data/lib/generators/rhino/dev/setup/setup_generator.rb +199 -0
  57. data/lib/generators/rhino/dev/setup/templates/env.client.tt +4 -0
  58. data/lib/generators/rhino/dev/setup/templates/env.root.tt +1 -0
  59. data/lib/generators/rhino/dev/setup/templates/env.server.tt +35 -0
  60. data/lib/generators/rhino/dev/setup/templates/prepare-commit-msg +17 -0
  61. data/lib/generators/rhino/install/install_generator.rb +24 -0
  62. data/lib/generators/rhino/install/templates/account.rb +4 -0
  63. data/lib/generators/rhino/install/templates/rhino.rb +24 -0
  64. data/lib/generators/rhino/install/templates/user.rb +4 -0
  65. data/lib/generators/rhino/model/model_generator.rb +96 -0
  66. data/lib/generators/rhino/module/USAGE +6 -0
  67. data/lib/generators/rhino/module/module_generator.rb +92 -0
  68. data/lib/generators/rhino/module/templates/%name%.gemspec.tt +24 -0
  69. data/lib/generators/rhino/module/templates/lib/%namespaced_name%/engine.rb.tt +18 -0
  70. data/lib/generators/rhino/module/templates/lib/generators/%namespaced_name%/install/install_generator.rb.tt +12 -0
  71. data/lib/generators/rhino/module/templates/lib/tasks/%namespaced_name%_tasks.rake.tt +13 -0
  72. data/lib/generators/rhino/module/templates/test/dummy/app/models/user.rb +4 -0
  73. data/lib/generators/rhino/module/templates/test/dummy/config/database.yml +25 -0
  74. data/lib/generators/rhino/module/templates/test/dummy/config/initializers/devise.rb +311 -0
  75. data/lib/generators/rhino/module/templates/test/dummy/config/initializers/devise_token_auth.rb +71 -0
  76. data/lib/generators/rhino/module/templates/test/test_helper.rb +54 -0
  77. data/lib/generators/rhino/policy/policy_generator.rb +33 -0
  78. data/lib/generators/rhino/policy/templates/policy.rb.tt +46 -0
  79. data/lib/generators/test_unit/rhino_policy_generator.rb +13 -0
  80. data/lib/generators/test_unit/templates/policy_test.rb.tt +57 -0
  81. data/lib/rhino/engine.rb +166 -0
  82. data/lib/rhino/omniauth/strategies/azure_o_auth2.rb +16 -0
  83. data/lib/rhino/resource/active_model_extension/backing_store/google_sheet.rb +89 -0
  84. data/lib/rhino/resource/active_model_extension/backing_store.rb +33 -0
  85. data/lib/rhino/resource/active_model_extension/describe.rb +38 -0
  86. data/lib/rhino/resource/active_model_extension/params.rb +70 -0
  87. data/lib/rhino/resource/active_model_extension/properties.rb +231 -0
  88. data/lib/rhino/resource/active_model_extension/reference.rb +50 -0
  89. data/lib/rhino/resource/active_model_extension/routing.rb +15 -0
  90. data/lib/rhino/resource/active_model_extension/serialization.rb +16 -0
  91. data/lib/rhino/resource/active_model_extension.rb +38 -0
  92. data/lib/rhino/resource/active_record_extension/describe.rb +44 -0
  93. data/lib/rhino/resource/active_record_extension/params.rb +213 -0
  94. data/lib/rhino/resource/active_record_extension/properties.rb +85 -0
  95. data/lib/rhino/resource/active_record_extension/properties_describe.rb +228 -0
  96. data/lib/rhino/resource/active_record_extension/reference.rb +50 -0
  97. data/lib/rhino/resource/active_record_extension/routing.rb +21 -0
  98. data/lib/rhino/resource/active_record_extension/search.rb +23 -0
  99. data/lib/rhino/resource/active_record_extension/serialization.rb +16 -0
  100. data/lib/rhino/resource/active_record_extension/super_admin.rb +25 -0
  101. data/lib/rhino/resource/active_record_extension.rb +32 -0
  102. data/lib/rhino/resource/active_record_tree.rb +50 -0
  103. data/lib/rhino/resource/active_storage_extension.rb +41 -0
  104. data/lib/rhino/resource/describe.rb +19 -0
  105. data/lib/rhino/resource/owner.rb +172 -0
  106. data/lib/rhino/resource/params.rb +31 -0
  107. data/lib/rhino/resource/properties.rb +192 -0
  108. data/lib/rhino/resource/reference.rb +29 -0
  109. data/lib/rhino/resource/routing.rb +107 -0
  110. data/lib/rhino/resource/serialization.rb +13 -0
  111. data/lib/rhino/resource/sieves.rb +36 -0
  112. data/lib/rhino/resource.rb +55 -0
  113. data/lib/rhino/sieve/filter.rb +149 -0
  114. data/lib/rhino/sieve/geospatial.rb +45 -0
  115. data/lib/rhino/sieve/helpers.rb +11 -0
  116. data/lib/rhino/sieve/limit.rb +20 -0
  117. data/lib/rhino/sieve/offset.rb +16 -0
  118. data/lib/rhino/sieve/order.rb +143 -0
  119. data/lib/rhino/sieve/search.rb +20 -0
  120. data/lib/rhino/sieve.rb +159 -0
  121. data/lib/rhino/test_case/controller.rb +145 -0
  122. data/lib/rhino/test_case/model.rb +86 -0
  123. data/lib/rhino/test_case/override.rb +19 -0
  124. data/lib/rhino/test_case/policy.rb +76 -0
  125. data/lib/rhino/test_case.rb +11 -0
  126. data/lib/rhino/version.rb +17 -0
  127. data/lib/rhino_project_core.rb +131 -0
  128. data/lib/tasks/rhino.rake +24 -0
  129. data/lib/tasks/rhino_dev.rake +17 -0
  130. data/lib/validators/country_validator.rb +11 -0
  131. data/lib/validators/email_validator.rb +8 -0
  132. data/lib/validators/ipv4_validator.rb +10 -0
  133. data/lib/validators/mac_address_validator.rb +9 -0
  134. metadata +531 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Params
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do # rubocop:todo Metrics/BlockLength
10
+ def create_params
11
+ writeable_params("create")
12
+ end
13
+
14
+ def show_params
15
+ readable_params("show")
16
+ end
17
+
18
+ def update_params
19
+ writeable_params("update")
20
+ end
21
+
22
+ protected
23
+ def props_by_type(type)
24
+ # FIXME: Direct attributes for this model we want a copy, not to
25
+ # alter the class_attribute itself
26
+ send("#{type}_properties").dup
27
+ end
28
+
29
+ # FIXME: Refs are not handled
30
+ def readable_params(_type, _refs = references)
31
+ params = []
32
+ props_by_type("read").each do |prop|
33
+ desc = describe_property(prop)
34
+
35
+ # Generic array of scalars
36
+ next params << { prop => [] } if desc[:type] == :array
37
+
38
+ # Otherwise prop and param are equivalent
39
+ params << prop
40
+ end
41
+
42
+ # Display name is always allowed
43
+ params << "display_name"
44
+ end
45
+
46
+ # FIXME: Refs are not handled
47
+ def writeable_params(type, _refs = references)
48
+ params = []
49
+
50
+ props_by_type(type).each do |prop|
51
+ desc = describe_property(prop)
52
+
53
+ # Generic array of scalars
54
+ next params << { prop => [] } if desc[:type] == :array
55
+
56
+ # Otherwise prop and param are equivalent
57
+ # We also accept the ref name as the foreign key if its a singular resource
58
+ params << prop
59
+ end
60
+
61
+ # Allow id in if its an update so we can find the original record
62
+ params << identifier_property if type == "update"
63
+
64
+ params
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "js_regex"
4
+
5
+ module Rhino
6
+ module Resource
7
+ module ActiveModelExtension
8
+ module Properties # rubocop:disable Metrics/ModuleLength
9
+ extend ActiveSupport::Concern
10
+
11
+ class_methods do # rubocop:disable Metrics/BlockLength
12
+ def identifier_property
13
+ "id"
14
+ end
15
+
16
+ def readable_properties
17
+ props = attribute_names - foreign_key_properties
18
+
19
+ props.concat(reference_properties)
20
+
21
+ props.map(&:to_s)
22
+ end
23
+
24
+ def creatable_properties
25
+ writeable_properties
26
+ end
27
+
28
+ def updatable_properties
29
+ writeable_properties
30
+ end
31
+
32
+ def describe_property(property) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
33
+ name = property_name(property).to_s
34
+ {
35
+ "x-rhino-attribute": {
36
+ name:,
37
+ readableName: name.titleize,
38
+ readable: read_properties.include?(property),
39
+ creatable: create_properties.include?(property),
40
+ updatable: update_properties.include?(property)
41
+ },
42
+ readOnly: property_read_only?(name),
43
+ writeOnly: property_write_only?(name),
44
+ nullable: property_nullable?(name),
45
+ default: property_default(name)
46
+ }
47
+ .merge(property_type(property))
48
+ .merge(property_validations(property))
49
+ .compact
50
+ end
51
+
52
+ private
53
+ # FIXME: Duplicated in params.rb
54
+ def reference_to_sym(reference)
55
+ reference.is_a?(Hash) ? reference.keys.first : reference
56
+ end
57
+
58
+ def automatic_properties
59
+ [identifier_property]
60
+ end
61
+
62
+ def foreign_key_properties
63
+ # reflect_on_all_associations(:belongs_to).map(&:foreign_key).map(&:to_s)
64
+ []
65
+ end
66
+
67
+ def reference_properties(read = true) # rubocop:todo Style/OptionalBooleanParameter
68
+ references.filter_map do |r|
69
+ sym = reference_to_sym(r)
70
+
71
+ # All references are readable
72
+ next sym if read
73
+
74
+ # Writeable if a one type or accepting nested
75
+ association = reflect_on_association(sym)
76
+ # rubocop:todo Performance/CollectionLiteralInLoop
77
+ sym if %i[has_one belongs_to].include?(association.macro) || nested_attributes_options.key?(sym)
78
+ # rubocop:enable Performance/CollectionLiteralInLoop
79
+ end
80
+ end
81
+
82
+ def writeable_properties
83
+ # Direct properties for this model
84
+ props = attribute_names - automatic_properties - foreign_key_properties
85
+
86
+ props.concat(reference_properties(false))
87
+
88
+ props.map(&:to_s)
89
+ end
90
+
91
+ def property_name(property)
92
+ property.is_a?(Hash) ? property.keys.first : property
93
+ end
94
+
95
+ def ref_descriptor(name)
96
+ {
97
+ type: :reference,
98
+ anyOf: [
99
+ { :$ref => "#/components/schemas/#{name.singularize}" }
100
+ ]
101
+ }
102
+ end
103
+
104
+ def property_type_raw(property)
105
+ name = property_name(property)
106
+ return :identifier if name == identifier_property
107
+
108
+ # return :string if defined_enums.key?(name)
109
+
110
+ # FIXME: Hack for tags for now
111
+ # if attribute_types.key?(name.to_s) && attribute_types[name.to_s].class.to_s == 'ActsAsTaggableOn::Taggable::TagListType'
112
+ # return {
113
+ # type: :array,
114
+ # items: {
115
+ # type: 'string'
116
+ # }
117
+ # }
118
+ # end
119
+
120
+ # Use the attribute type if possible
121
+ return attribute_types[name.to_s].type if attribute_types.key?(name.to_s)
122
+
123
+ # if reflections.key?(name)
124
+ # # FIXME: The tr hack is to match how model_name in rails handles modularized classes
125
+ # class_name = reflections[name].options[:class_name]&.underscore&.tr('/', '_') || name
126
+ # return ref_descriptor(class_name) unless reflections[name].macro == :has_many
127
+ #
128
+ # return {
129
+ # type: :array,
130
+ # items: ref_descriptor(class_name)
131
+ # }
132
+ # end
133
+
134
+ # raise UnknownpropertyType
135
+ "unknown"
136
+ end
137
+
138
+ def property_type(property)
139
+ pt = property_type_raw(property)
140
+
141
+ return pt if pt.is_a? Hash
142
+
143
+ { type: property_type_raw(property) }
144
+ end
145
+
146
+ # rubocop:todo Metrics/PerceivedComplexity
147
+ def property_validations(property) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
148
+ constraint_hash = {}
149
+
150
+ # https://swagger.io/specification/
151
+
152
+ validators_on(property).each do |v|
153
+ if v.is_a? ActiveModel::Validations::NumericalityValidator
154
+ if v.options.key?(:greater_than)
155
+ constraint_hash[:minimum] = v.options[:greater_than]
156
+ constraint_hash[:exclusiveMinimum] = true
157
+ end
158
+
159
+ if v.options.key?(:less_than)
160
+ constraint_hash[:maximum] = v.options[:less_than]
161
+ constraint_hash[:exclusiveMaximum] = true
162
+ end
163
+
164
+ constraint_hash[:minimum] = v.options[:greater_than_or_equal_to] if v.options.key?(:greater_than_or_equal_to)
165
+ constraint_hash[:maximum] = v.options[:less_than_or_equal_to] if v.options.key?(:less_than_or_equal_to)
166
+
167
+ if v.options.key?(:in)
168
+ constraint_hash[:minimum] = v.options[:in].min
169
+ constraint_hash[:maximum] = v.options[:in].max
170
+ end
171
+ end
172
+
173
+ if v.is_a? ::ActiveModel::Validations::LengthValidator
174
+ constraint_hash[:minLength] = v.options[:minimum] || v.options[:is]
175
+ constraint_hash[:maxLength] = v.options[:maximum] || v.options[:is]
176
+ end
177
+
178
+ constraint_hash[:pattern] = JsRegex.new(v.options[:with]).source if v.is_a? ::ActiveModel::Validations::FormatValidator
179
+
180
+ constraint_hash[:enum] = v.options[:in] if v.is_a? ActiveModel::Validations::InclusionValidator
181
+ end
182
+
183
+ constraint_hash.compact
184
+ end
185
+ # rubocop:enable Metrics/PerceivedComplexity
186
+
187
+ # If there is a presence validator in the model it is not nullable.
188
+ # if there is no optional: true on an association, rails will add a
189
+ # presence validator automatically
190
+ # Otherwise check the db for the actual column or foreign key setting
191
+ # Return nil instead of false for compaction
192
+ def property_nullable?(name)
193
+ # Check for presence validator
194
+ if validators.select { |v| v.is_a? ::ActiveModel::Validations::PresenceValidator }.flat_map(&:attributes).include?(name.to_sym)
195
+ return false
196
+ end
197
+
198
+ # By default, numericality doesn't allow nil values. You can use allow_nil: true option to permit it.
199
+ validators_on(name).select { |v| v.is_a? ::ActiveModel::Validations::NumericalityValidator }.each do |v|
200
+ return false unless v.options[:allow_nil]
201
+ end
202
+
203
+ true
204
+ end
205
+
206
+ # Return nil instead of false for compaction
207
+ def property_read_only?(name)
208
+ return unless read_properties.include?(name) && (create_properties.exclude?(name) && update_properties.exclude?(name))
209
+
210
+ true
211
+ end
212
+
213
+ # Return nil instead of false for compaction
214
+ def property_write_only?(name)
215
+ return unless (create_properties.include?(name) || update_properties.include?(name)) && read_properties.exclude?(name)
216
+
217
+ true
218
+ end
219
+
220
+ def property_default(name)
221
+ # FIXME: This will not handle datetime fields
222
+ # https://github.com/rails/rails/issues/27077 sets the default in the db
223
+ # but Blog.new does not set the default value like other attributes
224
+ # https://nubinary.atlassian.net/browse/NUB-298
225
+ _default_attributes[name].type_cast(_default_attributes[name].value_before_type_cast)
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Reference
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ scope :eager_load_refs, -> { includes(klass.references) } if defined?(scope)
11
+
12
+ def references_for_serialization
13
+ serialize_references(references)
14
+ end
15
+
16
+ private
17
+ # FIXME: Duplicated in params.rb
18
+ def reference_to_sym(reference)
19
+ reference.is_a?(Hash) ? reference.keys.first : reference
20
+ end
21
+
22
+ # FIXME: Duplicated in params.rb
23
+ def reference_from_sym(sym)
24
+ ref = try(sym)
25
+ return unless ref
26
+
27
+ # This is mostly how serializable_hash does it
28
+ # Get the first object
29
+ return ref.first if ref.respond_to?(:to_ary)
30
+
31
+ ref
32
+ end
33
+
34
+ def serialize_references(references)
35
+ hash = {}
36
+ references.each do |ref_item|
37
+ sym = reference_to_sym(ref_item)
38
+
39
+ hash[sym] = {}
40
+ hash[sym][:methods] = :display_name
41
+ hash[sym][:include] = serialize_references(ref_item[sym]) if ref_item.is_a?(Hash)
42
+ end.flatten.compact
43
+
44
+ hash
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Routing
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ delegate :route_key, to: :model_name
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveModelExtension
6
+ module Serialization
7
+ extend ActiveSupport::Concern
8
+
9
+ def to_caching_json
10
+ serializable_hash(methods: :display_name, include: references_for_serialization)
11
+ # JSON.generate(hash)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../resource"
4
+ require_relative "active_model_extension/properties"
5
+ require_relative "active_model_extension/reference"
6
+ require_relative "active_model_extension/describe"
7
+ require_relative "active_model_extension/routing"
8
+ require_relative "active_model_extension/params"
9
+ require_relative "active_model_extension/serialization"
10
+ require_relative "active_model_extension/backing_store"
11
+
12
+ module Rhino
13
+ module Resource
14
+ module ActiveModelExtension
15
+ extend ActiveSupport::Concern
16
+
17
+ # The minimum needed to make this work
18
+ include ActiveModel::Model
19
+ include ActiveModel::Attributes
20
+ include ActiveModel::Serializers::JSON
21
+
22
+ # Base
23
+ include Rhino::Resource
24
+
25
+ # Active Model implementations
26
+ include Rhino::Resource::ActiveModelExtension::Properties
27
+ include Rhino::Resource::ActiveModelExtension::Reference
28
+ include Rhino::Resource::ActiveModelExtension::Describe
29
+ include Rhino::Resource::ActiveModelExtension::Routing
30
+ include Rhino::Resource::ActiveModelExtension::Params
31
+ include Rhino::Resource::ActiveModelExtension::Serialization
32
+
33
+ included do
34
+ rhino_controller :active_model_extension
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveRecordExtension
6
+ module Describe
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def describe # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
11
+ properties = all_properties.index_with { |p| describe_property(p) }
12
+
13
+ required = properties.reject { |_p, d| d[:nullable] || d[:readOnly] }.keys
14
+ # required: [] is not valid and will be compacted away
15
+ required = nil unless required.present?
16
+
17
+ {
18
+ "x-rhino-model": {
19
+ model: model_name.singular,
20
+ modelPlural: model_name.collection,
21
+ name: model_name.name.camelize(:lower),
22
+ pluralName: model_name.name.camelize(:lower).pluralize,
23
+ readableName: model_name.human,
24
+ pluralReadableName: model_name.human.pluralize,
25
+ ownedBy: resource_owned_by,
26
+ singular: route_singular?,
27
+ path: route_api,
28
+ searchable: searchable?
29
+ },
30
+ type: :object,
31
+ properties:,
32
+ required:
33
+ }.compact
34
+ end
35
+
36
+ # returns true if the model's rhino_search is set with at least one field
37
+ def searchable?
38
+ @rhino_is_searchable == true
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ module Resource
5
+ module ActiveRecordExtension
6
+ module Params # rubocop:todo Metrics/ModuleLength
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do # rubocop:todo Metrics/BlockLength
10
+ def create_params
11
+ writeable_params("create")
12
+ end
13
+
14
+ def show_params
15
+ readable_params("show")
16
+ end
17
+
18
+ def update_params
19
+ writeable_params("update")
20
+ end
21
+
22
+ def transform_params(params)
23
+ transform_params_recursive(params)
24
+ end
25
+
26
+ protected
27
+ def reference_to_sym(reference)
28
+ reference.is_a?(Hash) ? reference.keys.first : reference
29
+ end
30
+
31
+ def assoc_from_sym(sym, base = self)
32
+ assoc = base.reflect_on_association(sym)
33
+
34
+ # Delegate types support bolstered by rhino/rhino/app/overrides/active_record/delegated_type_override.rb
35
+ return assoc.active_record.send("#{assoc.name}_types").map(&:constantize) if assoc.options[:polymorphic]
36
+
37
+ [assoc.klass]
38
+ end
39
+
40
+ def props_by_type(type)
41
+ # FIXME: Direct attributes for this model we want a copy, not to
42
+ # alter the class_attribute itself
43
+ send("#{type}_properties").dup
44
+ end
45
+
46
+ def readable_params(type, refs = references) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
47
+ params = []
48
+
49
+ refs_index = refs.index_by { |r| reference_to_sym(r) }
50
+
51
+ props_by_type("read").each do |prop|
52
+ desc = describe_property(prop)
53
+ prop_sym = prop.to_sym
54
+
55
+ # If its a reference or an array of references
56
+ # FIXME: anyOf is a hack for now
57
+ if desc[:type] == :reference || (desc[:type] == :array && (desc[:items].key?(:$ref) || desc[:items].key?(:anyOf)))
58
+ next unless refs_index.key?(prop_sym)
59
+
60
+ next_refs = refs_index[prop_sym].is_a?(Hash) ? refs_index[prop_sym][prop_sym] : []
61
+ klasses = assoc_from_sym(prop_sym)
62
+
63
+ assoc_params = klasses.map { |klass| klass.send(:readable_params, type, next_refs) }
64
+
65
+ next params << { prop.to_s => assoc_params.flatten.uniq }
66
+ end
67
+
68
+ # JSON columns need special handling - allow all the nested params
69
+ next params << { prop => {} } if desc[:type].in?(%i[json jsonb])
70
+
71
+ # Generic array of scalars
72
+ next params << { prop => [] } if desc[:type] == :array
73
+
74
+ # Otherwise prop and param are equivalent
75
+ params << prop
76
+ end
77
+
78
+ # Display name is always allowed
79
+ params << "display_name"
80
+ end
81
+
82
+ def writeable_params(type, _refs = references) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
83
+ params = []
84
+
85
+ props_by_type(type).each do |prop| # rubocop:disable Metrics/BlockLength
86
+ desc = describe_property(prop)
87
+ prop_sym = prop.to_sym
88
+
89
+ # An array of references
90
+ if desc[:type] == :array && (desc[:items].key?(:$ref) || desc[:items].key?(:anyOf))
91
+ # FIXME: Hack for has_many_attached
92
+ next params << { prop => [] } if desc.dig(:items, :anyOf)[0]&.dig(:$ref) == "#/components/schemas/active_storage_attachment"
93
+
94
+ # We only accept if the active record accepts it
95
+ next unless nested_attributes_options.key?(prop_sym) || desc.dig(:items, :anyOf)[0]&.dig(:$ref)
96
+
97
+ assoc = assoc_from_sym(prop_sym).first
98
+
99
+ # This does not handle nested for a polymorphic model, but neither does rails
100
+ # FIXME: Do we need to handle :update_only option?
101
+ # FIXME: If a nested resource attribute is create only, the backend will allow it to be
102
+ # updated because create/update are merged together - the frontedn UI shows the right
103
+ # thing though NUB-844
104
+ assoc_params = []
105
+
106
+ array_attributes = desc[:items][:"x-rhino-attribute-array"]
107
+
108
+ if array_attributes[:updatable]
109
+ # If the attribute is updatable, we need to accept the id param
110
+ assoc_params << assoc.identifier_property
111
+
112
+ # If the attribute is updatable, we need to accept its updatable params
113
+ assoc_params << assoc.send(:writeable_params, "update")
114
+ end
115
+
116
+ # If the attribute is creatable, we need to accept its creatable params
117
+ assoc_params << assoc.send(:writeable_params, "create") if array_attributes[:creatable]
118
+
119
+ # If its destroyable, accept the _destroy param
120
+ assoc_params << "_destroy" if nested_attributes_options[prop_sym][:allow_destroy]
121
+
122
+ next params << { prop => assoc_params.flatten.uniq }
123
+ end
124
+
125
+ # JSON columns need special handling - allow all the nested params
126
+ next params << { prop => {} } if desc[:type].in?(%i[json jsonb])
127
+
128
+ # Generic array of scalars
129
+ next params << { prop => [] } if desc[:type] == :array
130
+
131
+ # Accept { blog_post: { :id }} as well as { blog_post: 3 } below
132
+ if desc[:type] == :reference
133
+ klasses = assoc_from_sym(prop_sym)
134
+
135
+ params << if nested_attributes_options.key?(prop_sym)
136
+ assoc_params = klasses.map(&:identifier_property)
137
+ assoc_params << klasses.map { |klass| klass.send(:writeable_params, "update") }
138
+
139
+ { prop => assoc_params.flatten.uniq }
140
+ else
141
+ { prop => klasses.map(&:identifier_property).uniq }
142
+ end
143
+ end
144
+
145
+ # Otherwise prop and param are equivalent
146
+ # We also accept the ref name as the foreign key if its a singular resource
147
+ params << prop
148
+ end
149
+
150
+ params
151
+ end
152
+
153
+ # Rebuild the params
154
+ # rubocop:todo Metrics/CyclomaticComplexity
155
+ def transform_params_recursive(params, parent = self) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
156
+ hash = {}
157
+ params.each do |param_key, param_value|
158
+ association = parent.reflect_on_association(param_key)
159
+
160
+ # Its a regular attribute
161
+ next hash[param_key] = param_value unless association
162
+
163
+ # FIXME
164
+ # Hack to rewrite for attachment/attachments and guard against object resubmission
165
+ if param_key.end_with?("_attachment")
166
+ hash[param_key.remove("_attachment")] = param_value if param_value.is_a?(String) || param_value.nil?
167
+
168
+ next
169
+ end
170
+ if param_key.end_with?("_attachments")
171
+ hash[param_key.remove("_attachments")] = param_value if param_value.is_a?(Array) || param_value.nil?
172
+
173
+ next
174
+ end
175
+
176
+ # Transform the nested attributes as well
177
+ # Nested need _attributes - we don't want the client to have to do that
178
+ if parent.nested_attributes_options.key?(param_key.to_sym)
179
+ attr_key = "#{param_key}_attributes"
180
+
181
+ # has_many nested should be an array
182
+ if association.macro == :has_many
183
+ next hash[attr_key] = param_value.map { |pv| parent.transform_params_recursive(pv, association.klass) }
184
+ end
185
+
186
+ # has_one/belongs_to is just the values
187
+ # if its a cardinal though, such as blog: 1 instead of blog: {name : 'my blog' }
188
+ # fallback to transforming to the foreign key
189
+ if param_value.is_a?(ActionController::Parameters)
190
+ klasses = assoc_from_sym(param_key, parent)
191
+
192
+ next hash[attr_key] = klasses.map { |klass| parent.transform_params_recursive(param_value, klass) }.reduce(:merge)
193
+ end
194
+ end
195
+
196
+ # Map association name to foreign key, ie blog => blog_id
197
+ # or blog: { id: } => blog_id
198
+ if param_value.is_a?(ActionController::Parameters)
199
+ next hash[association.foreign_key] = param_value[association.klass.identifier_property]
200
+ end
201
+
202
+ hash[association.foreign_key] = param_value
203
+ end
204
+
205
+ # Force permit since we should have already been permitted at this point
206
+ ActionController::Parameters.new(hash).permit!
207
+ end
208
+ # rubocop:enable Metrics/CyclomaticComplexity
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end