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.
- checksums.yaml +4 -4
- data/.cursor/rules/release.mdc +62 -0
- data/.gitignore +26 -0
- data/.rspec +3 -0
- data/.rubocop.yml +76 -107
- data/.travis.yml +7 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +321 -0
- data/README.md +1508 -136
- data/Rakefile +3 -14
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/jpie.gemspec +21 -38
- data/kiln/app/resources/user_message_resource.rb +4 -0
- data/lib/jpie.rb +3 -25
- data/lib/json_api/active_storage/deserialization.rb +116 -0
- data/lib/json_api/active_storage/detection.rb +69 -0
- data/lib/json_api/active_storage/serialization.rb +34 -0
- data/lib/json_api/configuration.rb +57 -0
- data/lib/json_api/controllers/base_controller.rb +26 -0
- data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
- data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
- data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
- data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
- data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
- data/lib/json_api/controllers/concerns/controller_helpers.rb +19 -0
- data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
- data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
- data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
- data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
- data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
- data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
- data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
- data/lib/json_api/controllers/concerns/relationships_controller/active_storage_removal.rb +67 -0
- data/lib/json_api/controllers/concerns/relationships_controller/events.rb +44 -0
- data/lib/json_api/controllers/concerns/relationships_controller/removal.rb +92 -0
- data/lib/json_api/controllers/concerns/relationships_controller/response_helpers.rb +55 -0
- data/lib/json_api/controllers/concerns/relationships_controller/serialization.rb +72 -0
- data/lib/json_api/controllers/concerns/relationships_controller/sorting.rb +114 -0
- data/lib/json_api/controllers/concerns/relationships_controller/updating.rb +73 -0
- data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
- data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
- data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
- data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
- data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
- data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
- data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
- data/lib/json_api/controllers/concerns/resource_actions.rb +106 -0
- data/lib/json_api/controllers/relationships_controller.rb +108 -0
- data/lib/json_api/controllers/resources_controller.rb +6 -0
- data/lib/json_api/errors/parameter_not_allowed.rb +19 -0
- data/lib/json_api/railtie.rb +112 -0
- data/lib/json_api/resources/active_storage_blob_resource.rb +19 -0
- data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
- data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
- data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
- data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
- data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
- data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
- data/lib/json_api/resources/resource.rb +32 -0
- data/lib/json_api/resources/resource_loader.rb +35 -0
- data/lib/json_api/routing.rb +81 -0
- data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
- data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
- data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
- data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
- data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
- data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
- data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
- data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
- data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
- data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
- data/lib/json_api/serialization/deserializer.rb +26 -0
- data/lib/json_api/serialization/serializer.rb +77 -0
- data/lib/json_api/support/active_storage_support.rb +82 -0
- data/lib/json_api/support/collection_query.rb +50 -0
- data/lib/json_api/support/concerns/condition_building.rb +57 -0
- data/lib/json_api/support/concerns/nested_filters.rb +130 -0
- data/lib/json_api/support/concerns/pagination.rb +30 -0
- data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
- data/lib/json_api/support/concerns/regular_filters.rb +81 -0
- data/lib/json_api/support/concerns/sorting.rb +88 -0
- data/lib/json_api/support/instrumentation.rb +43 -0
- data/lib/json_api/support/param_helpers.rb +54 -0
- data/lib/json_api/support/relationship_guard.rb +16 -0
- data/lib/json_api/support/relationship_helpers.rb +76 -0
- data/lib/json_api/support/resource_identifier.rb +87 -0
- data/lib/json_api/support/responders.rb +100 -0
- data/lib/json_api/support/response_helpers.rb +10 -0
- data/lib/json_api/support/sort_parsing.rb +21 -0
- data/lib/json_api/support/type_conversion.rb +21 -0
- data/lib/json_api/testing/test_helper.rb +76 -0
- data/lib/json_api/testing.rb +3 -0
- data/lib/{jpie → json_api}/version.rb +2 -2
- data/lib/json_api.rb +50 -0
- data/lib/rubocop/cop/custom/hash_value_omission.rb +53 -0
- metadata +100 -169
- data/.cursor/rules/dependencies.mdc +0 -19
- data/.cursor/rules/examples.mdc +0 -16
- data/.cursor/rules/git.mdc +0 -14
- data/.cursor/rules/project_structure.mdc +0 -30
- data/.cursor/rules/publish_gem.mdc +0 -73
- data/.cursor/rules/security.mdc +0 -14
- data/.cursor/rules/style.mdc +0 -15
- data/.cursor/rules/testing.mdc +0 -16
- data/.overcommit.yml +0 -35
- data/CHANGELOG.md +0 -164
- data/LICENSE.txt +0 -21
- data/PUBLISHING.md +0 -111
- data/examples/basic_example.md +0 -146
- data/examples/including_related_resources.md +0 -491
- data/examples/pagination.md +0 -303
- data/examples/relationships.md +0 -114
- data/examples/resource_attribute_configuration.md +0 -147
- data/examples/resource_meta_configuration.md +0 -244
- data/examples/rspec_testing.md +0 -130
- data/examples/single_table_inheritance.md +0 -160
- data/lib/jpie/configuration.rb +0 -12
- data/lib/jpie/controller/crud_actions.rb +0 -141
- data/lib/jpie/controller/error_handling/handler_setup.rb +0 -124
- data/lib/jpie/controller/error_handling/handlers.rb +0 -109
- data/lib/jpie/controller/error_handling.rb +0 -23
- data/lib/jpie/controller/json_api_validation.rb +0 -193
- data/lib/jpie/controller/parameter_parsing.rb +0 -78
- data/lib/jpie/controller/related_actions.rb +0 -45
- data/lib/jpie/controller/relationship_actions.rb +0 -291
- data/lib/jpie/controller/relationship_validation.rb +0 -117
- data/lib/jpie/controller/rendering.rb +0 -154
- data/lib/jpie/controller.rb +0 -45
- data/lib/jpie/deserializer.rb +0 -110
- data/lib/jpie/errors.rb +0 -117
- data/lib/jpie/generators/resource_generator.rb +0 -116
- data/lib/jpie/generators/templates/resource.rb.erb +0 -31
- data/lib/jpie/railtie.rb +0 -42
- data/lib/jpie/resource/attributable.rb +0 -112
- data/lib/jpie/resource/inferrable.rb +0 -43
- data/lib/jpie/resource/sortable.rb +0 -93
- data/lib/jpie/resource.rb +0 -147
- data/lib/jpie/routing.rb +0 -59
- 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
|