jpie 0.4.5 → 1.0.1

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 (141) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/release.mdc +62 -0
  3. data/.gitignore +26 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +76 -107
  6. data/.travis.yml +7 -0
  7. data/Gemfile +23 -0
  8. data/Gemfile.lock +321 -0
  9. data/README.md +1508 -136
  10. data/Rakefile +3 -14
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/jpie.gemspec +21 -38
  14. data/kiln/app/resources/user_message_resource.rb +4 -0
  15. data/lib/jpie.rb +3 -25
  16. data/lib/json_api/active_storage/deserialization.rb +116 -0
  17. data/lib/json_api/active_storage/detection.rb +69 -0
  18. data/lib/json_api/active_storage/serialization.rb +34 -0
  19. data/lib/json_api/configuration.rb +57 -0
  20. data/lib/json_api/controllers/base_controller.rb +26 -0
  21. data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
  22. data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
  23. data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
  24. data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
  25. data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
  26. data/lib/json_api/controllers/concerns/controller_helpers.rb +19 -0
  27. data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
  28. data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
  29. data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
  30. data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
  31. data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
  32. data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
  33. data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
  34. data/lib/json_api/controllers/concerns/relationships_controller/active_storage_removal.rb +67 -0
  35. data/lib/json_api/controllers/concerns/relationships_controller/events.rb +44 -0
  36. data/lib/json_api/controllers/concerns/relationships_controller/removal.rb +92 -0
  37. data/lib/json_api/controllers/concerns/relationships_controller/response_helpers.rb +55 -0
  38. data/lib/json_api/controllers/concerns/relationships_controller/serialization.rb +72 -0
  39. data/lib/json_api/controllers/concerns/relationships_controller/sorting.rb +114 -0
  40. data/lib/json_api/controllers/concerns/relationships_controller/updating.rb +73 -0
  41. data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
  42. data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
  43. data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
  44. data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
  45. data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
  46. data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
  47. data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
  48. data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
  49. data/lib/json_api/controllers/concerns/resource_actions.rb +106 -0
  50. data/lib/json_api/controllers/relationships_controller.rb +108 -0
  51. data/lib/json_api/controllers/resources_controller.rb +6 -0
  52. data/lib/json_api/errors/parameter_not_allowed.rb +19 -0
  53. data/lib/json_api/railtie.rb +112 -0
  54. data/lib/json_api/resources/active_storage_blob_resource.rb +19 -0
  55. data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
  56. data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
  57. data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
  58. data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
  59. data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
  60. data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
  61. data/lib/json_api/resources/resource.rb +32 -0
  62. data/lib/json_api/resources/resource_loader.rb +35 -0
  63. data/lib/json_api/routing.rb +81 -0
  64. data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
  65. data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
  66. data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
  67. data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
  68. data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
  69. data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
  70. data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
  71. data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
  72. data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
  73. data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
  74. data/lib/json_api/serialization/deserializer.rb +26 -0
  75. data/lib/json_api/serialization/serializer.rb +77 -0
  76. data/lib/json_api/support/active_storage_support.rb +82 -0
  77. data/lib/json_api/support/collection_query.rb +50 -0
  78. data/lib/json_api/support/concerns/condition_building.rb +57 -0
  79. data/lib/json_api/support/concerns/nested_filters.rb +130 -0
  80. data/lib/json_api/support/concerns/pagination.rb +30 -0
  81. data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
  82. data/lib/json_api/support/concerns/regular_filters.rb +81 -0
  83. data/lib/json_api/support/concerns/sorting.rb +88 -0
  84. data/lib/json_api/support/instrumentation.rb +43 -0
  85. data/lib/json_api/support/param_helpers.rb +54 -0
  86. data/lib/json_api/support/relationship_guard.rb +16 -0
  87. data/lib/json_api/support/relationship_helpers.rb +76 -0
  88. data/lib/json_api/support/resource_identifier.rb +87 -0
  89. data/lib/json_api/support/responders.rb +100 -0
  90. data/lib/json_api/support/response_helpers.rb +10 -0
  91. data/lib/json_api/support/sort_parsing.rb +21 -0
  92. data/lib/json_api/support/type_conversion.rb +21 -0
  93. data/lib/json_api/testing/test_helper.rb +76 -0
  94. data/lib/json_api/testing.rb +3 -0
  95. data/lib/{jpie → json_api}/version.rb +2 -2
  96. data/lib/json_api.rb +50 -0
  97. data/lib/rubocop/cop/custom/hash_value_omission.rb +53 -0
  98. metadata +100 -169
  99. data/.cursor/rules/dependencies.mdc +0 -19
  100. data/.cursor/rules/examples.mdc +0 -16
  101. data/.cursor/rules/git.mdc +0 -14
  102. data/.cursor/rules/project_structure.mdc +0 -30
  103. data/.cursor/rules/publish_gem.mdc +0 -73
  104. data/.cursor/rules/security.mdc +0 -14
  105. data/.cursor/rules/style.mdc +0 -15
  106. data/.cursor/rules/testing.mdc +0 -16
  107. data/.overcommit.yml +0 -35
  108. data/CHANGELOG.md +0 -164
  109. data/LICENSE.txt +0 -21
  110. data/PUBLISHING.md +0 -111
  111. data/examples/basic_example.md +0 -146
  112. data/examples/including_related_resources.md +0 -491
  113. data/examples/pagination.md +0 -303
  114. data/examples/relationships.md +0 -114
  115. data/examples/resource_attribute_configuration.md +0 -147
  116. data/examples/resource_meta_configuration.md +0 -244
  117. data/examples/rspec_testing.md +0 -130
  118. data/examples/single_table_inheritance.md +0 -160
  119. data/lib/jpie/configuration.rb +0 -12
  120. data/lib/jpie/controller/crud_actions.rb +0 -141
  121. data/lib/jpie/controller/error_handling/handler_setup.rb +0 -124
  122. data/lib/jpie/controller/error_handling/handlers.rb +0 -109
  123. data/lib/jpie/controller/error_handling.rb +0 -23
  124. data/lib/jpie/controller/json_api_validation.rb +0 -193
  125. data/lib/jpie/controller/parameter_parsing.rb +0 -78
  126. data/lib/jpie/controller/related_actions.rb +0 -45
  127. data/lib/jpie/controller/relationship_actions.rb +0 -291
  128. data/lib/jpie/controller/relationship_validation.rb +0 -117
  129. data/lib/jpie/controller/rendering.rb +0 -154
  130. data/lib/jpie/controller.rb +0 -45
  131. data/lib/jpie/deserializer.rb +0 -110
  132. data/lib/jpie/errors.rb +0 -117
  133. data/lib/jpie/generators/resource_generator.rb +0 -116
  134. data/lib/jpie/generators/templates/resource.rb.erb +0 -31
  135. data/lib/jpie/railtie.rb +0 -42
  136. data/lib/jpie/resource/attributable.rb +0 -112
  137. data/lib/jpie/resource/inferrable.rb +0 -43
  138. data/lib/jpie/resource/sortable.rb +0 -93
  139. data/lib/jpie/resource.rb +0 -147
  140. data/lib/jpie/routing.rb +0 -59
  141. data/lib/jpie/serializer.rb +0 -205
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json_api/support/relationship_guard"
4
+ require_relative "concerns/attributes_deserialization"
5
+ require_relative "concerns/relationships_deserialization"
6
+ require_relative "concerns/model_attributes_transformation"
7
+ require_relative "concerns/relationship_processing"
8
+ require_relative "concerns/deserialization_helpers"
9
+
10
+ module JSONAPI
11
+ class Deserializer
12
+ include ActiveStorageSupport
13
+ include Serialization::AttributesDeserialization
14
+ include Serialization::RelationshipsDeserialization
15
+ include Serialization::ModelAttributesTransformation
16
+ include Serialization::RelationshipProcessing
17
+ include Serialization::DeserializationHelpers
18
+
19
+ def initialize(params, model_class:, action: :create)
20
+ @params = ParamHelpers.deep_symbolize_params(params)
21
+ @model_class = model_class
22
+ @definition = ResourceLoader.find_for_model(model_class)
23
+ @action = action.to_sym
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "concerns/attributes_serialization"
4
+ require_relative "concerns/relationships_serialization"
5
+ require_relative "concerns/links_serialization"
6
+ require_relative "concerns/includes_serialization"
7
+ require_relative "concerns/meta_serialization"
8
+
9
+ module JSONAPI
10
+ class Serializer
11
+ include ActiveStorageSupport
12
+ include Serialization::AttributesSerialization
13
+ include Serialization::RelationshipsSerialization
14
+ include Serialization::LinksSerialization
15
+ include Serialization::IncludesSerialization
16
+ include Serialization::MetaSerialization
17
+
18
+ JSONAPI_VERSION = "1.1"
19
+
20
+ def self.jsonapi_object
21
+ obj = { version: JSONAPI_VERSION }
22
+ obj[:meta] = JSONAPI.configuration.jsonapi_meta if JSONAPI.configuration.jsonapi_meta
23
+ obj
24
+ end
25
+
26
+ def initialize(record, definition: nil, base_definition: nil)
27
+ @record = record
28
+ @definition = definition || ResourceLoader.find_for_model(record.class)
29
+ @base_definition = base_definition
30
+ @sti_subclass = nil
31
+ end
32
+
33
+ def to_hash(include: [], fields: {}, document_meta: nil)
34
+ {
35
+ jsonapi: jsonapi_object,
36
+ data: serialize_record(fields),
37
+ included: serialize_included(include, fields),
38
+ meta: document_meta,
39
+ }.compact
40
+ end
41
+
42
+ def serialize_record(fields = {})
43
+ {
44
+ type: record_type,
45
+ id: record_id,
46
+ attributes: serialize_attributes(fields),
47
+ relationships: serialize_relationships,
48
+ links: serialize_links,
49
+ meta: record_meta,
50
+ }.compact
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :record, :definition
56
+
57
+ def base_definition
58
+ @base_definition ||= definition
59
+ end
60
+
61
+ def record_type
62
+ if definition.name.end_with?("Resource")
63
+ RelationshipHelpers.resource_type_name(definition)
64
+ else
65
+ record.class.name.underscore.pluralize
66
+ end
67
+ end
68
+
69
+ def record_id
70
+ record.id.to_s
71
+ end
72
+
73
+ def jsonapi_object
74
+ self.class.jsonapi_object
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module JSONAPI
6
+ module ActiveStorageSupport
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def active_storage_attachment?(association_name, model_class)
11
+ ActiveStorage::Detection.attachment?(association_name, model_class)
12
+ end
13
+
14
+ def active_storage_blob_type?(type)
15
+ ActiveStorage::Detection.blob_type?(type)
16
+ end
17
+ end
18
+
19
+ def active_storage_attachment?(association_name, model_class = nil)
20
+ resolved = model_class || resolve_model_class_for_attachment
21
+ self.class.active_storage_attachment?(association_name, resolved)
22
+ end
23
+
24
+ def resolve_model_class_for_attachment
25
+ return send(:model_class) if respond_to?(:model_class, true)
26
+ return send(:resource_model_class) if respond_to?(:resource_model_class, true)
27
+ return send(:resource).class if respond_to?(:resource, true) && send(:resource).respond_to?(:class)
28
+
29
+ raise NotImplementedError, "Must implement resource_model_class or provide model_class parameter"
30
+ end
31
+
32
+ def extract_active_storage_params_from_hash(params_hash, model_class)
33
+ ActiveStorage::Deserialization.extract_params_from_hash(params_hash, model_class)
34
+ end
35
+
36
+ def attach_active_storage_files(record, attachment_params, resource_class: nil)
37
+ ActiveStorage::Deserialization.attach_files(record, attachment_params, definition: resource_class)
38
+ end
39
+
40
+ def purge_on_nil_enabled?(attachment_name, resource_class)
41
+ ActiveStorage::Deserialization.purge_on_nil_enabled?(attachment_name, resource_class)
42
+ end
43
+
44
+ def append_only_enabled?(attachment_name, resource_class)
45
+ ActiveStorage::Deserialization.append_only_enabled?(attachment_name, resource_class)
46
+ end
47
+
48
+ def find_relationship_definition(attachment_name, resource_class)
49
+ ActiveStorage::Deserialization.find_relationship_definition(attachment_name, resource_class)
50
+ end
51
+
52
+ def filter_active_storage_from_includes(includes_hash, current_model_class)
53
+ ActiveStorage::Detection.filter_from_includes(includes_hash, current_model_class)
54
+ end
55
+
56
+ def filter_polymorphic_from_includes(includes_hash, current_model_class)
57
+ ActiveStorage::Detection.filter_polymorphic_from_includes(includes_hash, current_model_class)
58
+ end
59
+
60
+ def serialize_active_storage_relationship(attachment_name, record)
61
+ ActiveStorage::Serialization.serialize_relationship(attachment_name, record)
62
+ end
63
+
64
+ def serialize_blob_identifier(blob)
65
+ ActiveStorage::Serialization.serialize_blob_identifier(blob)
66
+ end
67
+
68
+ def process_active_storage_attachment(attrs, association_name, id_or_ids, singular:)
69
+ ActiveStorage::Deserialization.process_attachment(attrs, association_name, id_or_ids, singular:)
70
+ end
71
+
72
+ def find_blob_by_signed_id(signed_id)
73
+ ActiveStorage::Deserialization.find_blob_by_signed_id(signed_id)
74
+ end
75
+
76
+ private
77
+
78
+ def resource_model_class
79
+ raise NotImplementedError, "Must implement resource_model_class or override active_storage_attachment?"
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "concerns/sorting"
4
+ require_relative "concerns/regular_filters"
5
+ require_relative "concerns/nested_filters"
6
+ require_relative "concerns/polymorphic_filters"
7
+ require_relative "concerns/condition_building"
8
+ require_relative "concerns/pagination"
9
+
10
+ module JSONAPI
11
+ class CollectionQuery
12
+ include Support::Sorting
13
+ include Support::RegularFilters
14
+ include Support::NestedFilters
15
+ include Support::PolymorphicFilters
16
+ include Support::ConditionBuilding
17
+ include Support::Pagination
18
+
19
+ attr_reader :scope, :total_count, :pagination_applied
20
+
21
+ def initialize(scope, definition:, model_class:, filter_params:, sort_params:, page_params:)
22
+ @scope = scope
23
+ @definition = definition
24
+ @model_class = model_class
25
+ @filter_params = filter_params
26
+ @sort_params = sort_params
27
+ @page_params = page_params
28
+ @pagination_applied = page_params.present?
29
+ end
30
+
31
+ def execute
32
+ @scope = apply_filtering
33
+ has_virtual_sort = sort_params.any? { |sort_field| virtual_attribute_sort?(sort_field) }
34
+ @total_count = @scope.count unless has_virtual_sort
35
+ @scope = apply_sorting(@scope)
36
+ @total_count = @scope.count if has_virtual_sort && @scope.is_a?(Array)
37
+ @scope = apply_pagination
38
+ self
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :definition, :model_class, :filter_params, :sort_params, :page_params
44
+
45
+ def apply_filtering
46
+ scope = apply_nested_relationship_filters(@scope)
47
+ apply_regular_filters(scope)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ module ConditionBuilding
6
+ private
7
+
8
+ def build_condition(column, value, operator)
9
+ build_arel_condition(model_class, column, value, operator)
10
+ end
11
+
12
+ def normalize_filter_value_for_model(model, column, raw_value)
13
+ return nil unless column
14
+
15
+ value = raw_value.is_a?(Array) ? raw_value.first : raw_value
16
+ return nil if value.nil?
17
+
18
+ type = model.type_for_attribute(column.name)
19
+ type.cast(value)
20
+ end
21
+
22
+ def build_condition_for_model(model, column, value, operator)
23
+ build_arel_condition(model, column, value, operator)
24
+ end
25
+
26
+ def build_arel_condition(model, column, value, operator)
27
+ attr = model.arel_table[column.name]
28
+ build_operator_condition(attr, value, operator)
29
+ end
30
+
31
+ def build_operator_condition(attr, value, operator)
32
+ case operator
33
+ when :eq then attr.eq(value)
34
+ when :lt then attr.lt(value)
35
+ when :lte then attr.lteq(value)
36
+ when :gt then attr.gt(value)
37
+ when :gte then attr.gteq(value)
38
+ when :match then build_match_condition(attr, value)
39
+ end
40
+ end
41
+
42
+ def build_match_condition(attr, value)
43
+ pattern = "%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"
44
+ lower_attr = Arel::Nodes::NamedFunction.new("LOWER", [attr])
45
+ lower_attr.matches(pattern.downcase)
46
+ end
47
+
48
+ def apply_condition(scope, condition)
49
+ scope.where(condition)
50
+ end
51
+
52
+ def empty_filter_value?(filter_value)
53
+ filter_value.respond_to?(:empty?) ? filter_value.empty? : filter_value.nil?
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ module NestedFilters
6
+ def apply_nested_relationship_filters(scope)
7
+ return scope if filter_params.empty?
8
+
9
+ nested_filters = filter_params.select { |k, _v| k.to_s.include?(".") }
10
+ return scope if nested_filters.empty?
11
+
12
+ nested_filters.reduce(scope) do |current_scope, (filter_name, filter_value)|
13
+ apply_filter_for_path(current_scope, filter_name.to_s, filter_value)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def apply_filter_for_path(scope, filter_name, filter_value)
20
+ parts = filter_name.split(".")
21
+ return scope if parts.length < 2
22
+
23
+ relationship_chain = parts[0..-2]
24
+ leaf_filter = parts.last
25
+
26
+ result = traverse_relationship_chain(scope, relationship_chain, leaf_filter, filter_value)
27
+ return result[:scope] if result[:early_return]
28
+
29
+ apply_joined_filter(scope, result, relationship_chain:, leaf_filter:, filter_value:)
30
+ end
31
+
32
+ def traverse_relationship_chain(scope, relationship_chain, leaf_filter, filter_value)
33
+ current_model = model_class
34
+ current_definition = definition
35
+
36
+ relationship_chain.each do |relationship_name|
37
+ result = process_chain_step(scope, current_model, relationship_name, leaf_filter, filter_value)
38
+ return result if result[:early_return]
39
+
40
+ current_model = result[:model]
41
+ current_definition = result[:definition]
42
+ end
43
+
44
+ { model: current_model, definition: current_definition, early_return: false }
45
+ end
46
+
47
+ def process_chain_step(scope, current_model, relationship_name, leaf_filter, filter_value)
48
+ association = current_model.reflect_on_association(relationship_name.to_sym)
49
+ return { scope:, early_return: true } unless association
50
+
51
+ if association.polymorphic?
52
+ attributes = { leaf_filter => filter_value }
53
+ return { scope: apply_polymorphic_nested_filters(scope, association, relationship_name, attributes),
54
+ early_return: true, }
55
+ end
56
+
57
+ next_definition = JSONAPI::Resource.resource_for_model(association.klass)
58
+ return { scope:, early_return: true } unless next_definition
59
+
60
+ { model: association.klass, definition: next_definition, early_return: false }
61
+ end
62
+
63
+ def apply_joined_filter(scope, result, relationship_chain:, leaf_filter:, filter_value:)
64
+ join_hash = build_join_hash_for_chain(relationship_chain)
65
+ scope = scope.joins(join_hash) if join_hash.present?
66
+ apply_filter_on_model(scope, result[:model], result[:definition], leaf_filter, filter_value)
67
+ end
68
+
69
+ def build_join_hash_for_chain(chain)
70
+ return nil if chain.empty?
71
+
72
+ chain.reverse.reduce(nil) do |acc, name|
73
+ if acc.nil?
74
+ name.to_sym
75
+ else
76
+ { name.to_sym => acc }
77
+ end
78
+ end
79
+ end
80
+
81
+ def apply_filter_on_model(scope, target_model, target_resource, filter_name, filter_value)
82
+ return scope if empty_filter_value?(filter_value)
83
+
84
+ apply_column_operator_filter(scope, target_model, filter_name, filter_value) ||
85
+ apply_direct_column_filter(scope, target_model, filter_name, filter_value) ||
86
+ apply_scope_method_filter(scope, target_model, target_resource, filter_name, filter_value) ||
87
+ scope
88
+ end
89
+
90
+ def apply_column_operator_filter(scope, target_model, filter_name, filter_value)
91
+ column_filter = parse_column_filter(filter_name)
92
+ return nil unless column_filter
93
+
94
+ column = target_model.column_for_attribute(column_filter[:column])
95
+ return nil unless column
96
+
97
+ value = normalize_filter_value_for_model(target_model, column, filter_value)
98
+ return nil unless value
99
+
100
+ condition = build_condition_for_model(target_model, column, value, column_filter[:operator])
101
+ condition ? apply_condition(scope, condition) : nil
102
+ end
103
+
104
+ def apply_direct_column_filter(scope, target_model, filter_name, filter_value)
105
+ return nil unless target_model.column_names.include?(filter_name)
106
+
107
+ scope.where(target_model.table_name => { filter_name => filter_value })
108
+ end
109
+
110
+ def apply_scope_method_filter(scope, target_model, target_resource, filter_name, filter_value)
111
+ if target_model.respond_to?(filter_name.to_sym)
112
+ return try_scope_method(scope, target_model, filter_name,
113
+ filter_value,)
114
+ end
115
+
116
+ return nil unless target_resource
117
+ return nil unless target_resource.permitted_filters.map(&:to_s).include?(filter_name)
118
+ return nil unless target_model.respond_to?(filter_name.to_sym)
119
+
120
+ try_scope_method(scope, target_model, filter_name, filter_value)
121
+ end
122
+
123
+ def try_scope_method(scope, target_model, filter_name, filter_value)
124
+ scope.merge(target_model.public_send(filter_name.to_sym, filter_value))
125
+ rescue ArgumentError, NoMethodError
126
+ nil
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ module Pagination
6
+ def apply_pagination
7
+ return @scope if page_params.empty?
8
+
9
+ offset, size = calculate_pagination_params
10
+ paginate_scope(offset, size)
11
+ end
12
+
13
+ private
14
+
15
+ def calculate_pagination_params
16
+ number = page_params["number"]&.to_i || 1
17
+ size = page_params["size"]&.to_i || JSONAPI.configuration.default_page_size
18
+ size = [size, JSONAPI.configuration.max_page_size].min
19
+ offset = (number - 1) * size
20
+ [offset, size]
21
+ end
22
+
23
+ def paginate_scope(offset, size)
24
+ return @scope.slice(offset, size) || [] if @scope.is_a?(Array)
25
+
26
+ @scope.offset(offset).limit(size)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ module PolymorphicFilters
6
+ def apply_polymorphic_nested_filters(scope, association, _relationship_name, attributes)
7
+ context = build_polymorphic_filter_context(association, attributes)
8
+
9
+ attributes.each do |attr_name, attr_value|
10
+ scope = apply_polymorphic_attribute_filter(scope, attr_name, attr_value, context)
11
+ end
12
+
13
+ apply_final_type_filter(scope, context)
14
+ end
15
+
16
+ private
17
+
18
+ def build_polymorphic_filter_context(association, attributes)
19
+ {
20
+ foreign_key: association.foreign_key,
21
+ foreign_type: association.foreign_type,
22
+ fk_column: model_class.column_for_attribute(association.foreign_key),
23
+ type_value: attributes["type"] || attributes["type_eq"],
24
+ class_name: association.options[:class_name],
25
+ }
26
+ end
27
+
28
+ def apply_polymorphic_attribute_filter(scope, attr_name, attr_value, context)
29
+ return scope if empty_filter_value?(attr_value)
30
+ return scope if attr_name == "type"
31
+
32
+ scope = apply_polymorphic_id_filter(scope, attr_name, attr_value, context)
33
+ apply_polymorphic_type_filter(scope, context)
34
+ end
35
+
36
+ def apply_polymorphic_id_filter(scope, attr_name, attr_value, context)
37
+ column_filter = parse_column_filter(attr_name)
38
+ return apply_polymorphic_fk_filter(scope, attr_value, column_filter, context) if polymorphic_fk_filter?(
39
+ column_filter, context,
40
+ )
41
+ return scope.where(context[:foreign_key] => attr_value) if attr_name == "id"
42
+
43
+ scope
44
+ end
45
+
46
+ def polymorphic_fk_filter?(column_filter, context)
47
+ column_filter && column_filter[:column] == "id" && context[:fk_column]
48
+ end
49
+
50
+ def apply_polymorphic_fk_filter(scope, attr_value, column_filter, context)
51
+ value = normalize_filter_value_for_model(model_class, context[:fk_column], attr_value)
52
+ return scope unless value
53
+
54
+ condition = build_condition_for_model(model_class, context[:fk_column], value, column_filter[:operator])
55
+ condition ? apply_condition(scope, condition) : scope
56
+ end
57
+
58
+ def apply_polymorphic_type_filter(scope, context)
59
+ return scope unless context[:foreign_type]
60
+ return scope.where(context[:foreign_type] => context[:class_name]) if context[:class_name]
61
+ return scope.where(context[:foreign_type] => context[:type_value]) if context[:type_value]
62
+
63
+ scope
64
+ end
65
+
66
+ def apply_final_type_filter(scope, context)
67
+ return scope unless context[:foreign_type]
68
+ return scope if context[:class_name]
69
+ return scope unless context[:type_value]
70
+
71
+ scope.where(context[:foreign_type] => context[:type_value])
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ module RegularFilters
6
+ def apply_regular_filters(scope)
7
+ return scope if filter_params.empty?
8
+
9
+ regular_filters = filter_params.reject { |k, _v| k.to_s.include?(".") }
10
+ return scope if regular_filters.empty?
11
+
12
+ regular_filters.reduce(scope) do |current_scope, (filter_name, filter_value)|
13
+ apply_regular_filter(current_scope, filter_name.to_s, filter_value)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def apply_regular_filter(scope, filter_name, filter_value)
20
+ return scope if filter_value.respond_to?(:empty?) ? filter_value.empty? : filter_value.nil?
21
+
22
+ column_filter = parse_column_filter(filter_name)
23
+ if column_filter
24
+ apply_column_filter(scope, column_filter, filter_value)
25
+ else
26
+ apply_scope_fallback(scope, filter_name, filter_value)
27
+ end
28
+ end
29
+
30
+ def parse_column_filter(filter_name)
31
+ match = filter_name.match(/\A(.+)_(eq|match|lt|lte|gt|gte)\z/)
32
+ return nil unless match
33
+
34
+ column_name = match[1]
35
+ operator = match[2].to_sym
36
+
37
+ { column: column_name, operator: }
38
+ end
39
+
40
+ def apply_column_filter(scope, column_filter, raw_value)
41
+ condition = build_column_condition(column_filter, raw_value)
42
+ condition ? apply_condition(scope, condition) : scope
43
+ rescue StandardError => e
44
+ log_filter_error(column_filter, column_filter[:operator], e)
45
+ scope
46
+ end
47
+
48
+ def build_column_condition(column_filter, raw_value)
49
+ column = model_class.column_for_attribute(column_filter[:column])
50
+ return nil unless column
51
+
52
+ value = normalize_filter_value(column, raw_value)
53
+ return nil if value.nil?
54
+
55
+ build_condition(column, value, column_filter[:operator])
56
+ end
57
+
58
+ def log_filter_error(column_filter, operator, error)
59
+ return unless defined?(Rails.logger)
60
+
61
+ Rails.logger.warn("Filter error for #{column_filter[:column]}_#{operator}: #{error.class} - #{error.message}")
62
+ end
63
+
64
+ def normalize_filter_value(column, raw_value)
65
+ value = raw_value.is_a?(Array) ? raw_value.first : raw_value
66
+ return nil if value.nil?
67
+
68
+ type = model_class.type_for_attribute(column.name)
69
+ type.cast(value)
70
+ end
71
+
72
+ def apply_scope_fallback(scope, filter_name, filter_value)
73
+ return scope unless model_class.respond_to?(filter_name.to_sym)
74
+
75
+ scope.public_send(filter_name.to_sym, filter_value)
76
+ rescue ArgumentError, NoMethodError
77
+ scope
78
+ end
79
+ end
80
+ end
81
+ end