From 0420218296fc07a3da1df409d41b01d34d273c28 Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Fri, 6 May 2016 14:25:43 +0100 Subject: [PATCH 01/11] The relationships for a class are now available in the instance. This change allows a resource to add to the relationships in the instance - for "virtual" relationships that are not defined in the model. --- lib/jsonapi/exceptions.rb | 2 +- lib/jsonapi/resource.rb | 26 +++++++++++++++----------- lib/jsonapi/resource_serializer.rb | 2 +- lib/jsonapi/response_document.rb | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/jsonapi/exceptions.rb b/lib/jsonapi/exceptions.rb index 0670ede24..12179d728 100644 --- a/lib/jsonapi/exceptions.rb +++ b/lib/jsonapi/exceptions.rb @@ -360,7 +360,7 @@ class ValidationErrors < Error def initialize(resource) @error_messages = resource.model_error_messages @error_metadata = resource.validation_error_metadata - @resource_relationships = resource.class._relationships.keys + @resource_relationships = resource._relationships.keys @key_formatter = JSONAPI.configuration.key_formatter end diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 05a7795de..fe83ab950 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -161,6 +161,10 @@ def custom_links(_options) {} end + def _relationships + self.class._relationships + end + private def save @@ -213,7 +217,7 @@ def _remove end def _create_to_many_links(relationship_type, relationship_key_values) - relationship = self.class._relationships[relationship_type] + relationship = _relationships[relationship_type] relationship_key_values.each do |relationship_key_value| related_resource = relationship.resource_klass.find_by_key(relationship_key_value, context: @context) @@ -232,7 +236,7 @@ def _create_to_many_links(relationship_type, relationship_key_values) end def _replace_to_many_links(relationship_type, relationship_key_values) - relationship = self.class._relationships[relationship_type] + relationship = _relationships[relationship_type] send("#{relationship.foreign_key}=", relationship_key_values) @save_needed = true @@ -240,7 +244,7 @@ def _replace_to_many_links(relationship_type, relationship_key_values) end def _replace_to_one_link(relationship_type, relationship_key_value) - relationship = self.class._relationships[relationship_type] + relationship = _relationships[relationship_type] send("#{relationship.foreign_key}=", relationship_key_value) @save_needed = true @@ -249,7 +253,7 @@ def _replace_to_one_link(relationship_type, relationship_key_value) end def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type) - relationship = self.class._relationships[relationship_type.to_sym] + relationship = _relationships[relationship_type.to_sym] _model.public_send("#{relationship.foreign_key}=", key_value) _model.public_send("#{relationship.polymorphic_type}=", key_type.to_s.classify) @@ -260,7 +264,7 @@ def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type) end def _remove_to_many_link(relationship_type, key) - relation_name = self.class._relationships[relationship_type].relation_name(context: @context) + relation_name = _relationships[relationship_type].relation_name(context: @context) @model.public_send(relation_name).delete(key) @@ -268,7 +272,7 @@ def _remove_to_many_link(relationship_type, key) end def _remove_to_one_link(relationship_type) - relationship = self.class._relationships[relationship_type] + relationship = _relationships[relationship_type] send("#{relationship.foreign_key}=", nil) @save_needed = true @@ -875,7 +879,7 @@ def _add_relationship(klass, *attrs) end unless method_defined?("#{foreign_key}=") define_method associated_records_method_name do - relationship = self.class._relationships[relationship_name] + relationship = _relationships[relationship_name] relation_name = relationship.relation_name(context: @context) records_for(relation_name) end unless method_defined?(associated_records_method_name) @@ -887,7 +891,7 @@ def _add_relationship(klass, *attrs) end unless method_defined?(foreign_key) define_method relationship_name do |options = {}| - relationship = self.class._relationships[relationship_name] + relationship = _relationships[relationship_name] if relationship.polymorphic? associated_model = public_send(associated_records_method_name) @@ -903,7 +907,7 @@ def _add_relationship(klass, *attrs) end unless method_defined?(relationship_name) else define_method foreign_key do - relationship = self.class._relationships[relationship_name] + relationship = _relationships[relationship_name] record = public_send(associated_records_method_name) return nil if record.nil? @@ -911,7 +915,7 @@ def _add_relationship(klass, *attrs) end unless method_defined?(foreign_key) define_method relationship_name do |options = {}| - relationship = self.class._relationships[relationship_name] + relationship = _relationships[relationship_name] resource_klass = relationship.resource_klass if resource_klass @@ -929,7 +933,7 @@ def _add_relationship(klass, *attrs) end unless method_defined?(foreign_key) define_method relationship_name do |options = {}| - relationship = self.class._relationships[relationship_name] + relationship = _relationships[relationship_name] resource_klass = relationship.resource_klass records = public_send(associated_records_method_name) diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index aad37dd36..2aa0a74c1 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -189,7 +189,7 @@ def self_referential_and_already_in_source(resource) end def relationships_hash(source, include_directives) - relationships = source.class._relationships + relationships = source._relationships requested = requested_fields(source.class) fields = relationships.keys fields = requested & fields unless requested.nil? diff --git a/lib/jsonapi/response_document.rb b/lib/jsonapi/response_document.rb index 9d3f7e8c5..fde0fdf60 100644 --- a/lib/jsonapi/response_document.rb +++ b/lib/jsonapi/response_document.rb @@ -77,7 +77,7 @@ def top_level_links if result.is_a?(JSONAPI::ResourcesOperationResult) || result.is_a?(JSONAPI::RelatedResourcesOperationResult) result.pagination_params.each_pair do |link_name, params| if result.is_a?(JSONAPI::RelatedResourcesOperationResult) - relationship = result.source_resource.class._relationships[result._type.to_sym] + relationship = result.source_resource._relationships[result._type.to_sym] links[link_name] = serializer.link_builder.relationships_related_link(result.source_resource, relationship, query_params(params)) else links[link_name] = serializer.query_link(query_params(params)) From 19d584fc76972ea184c3f93014ff3b520ef322ac Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Fri, 6 May 2016 15:38:50 +0100 Subject: [PATCH 02/11] Allows includes through without validation if config option is set This is paving the way for dynamic virtual relationships based on the model data --- .gitignore | 3 ++- lib/jsonapi/configuration.rb | 6 ++++-- lib/jsonapi/request.rb | 4 ++-- lib/jsonapi/resource.rb | 9 +++++---- test/controllers/controller_test.rb | 9 +++++++++ test/integration/requests/request_test.rb | 8 ++++++++ 6 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 44b605873..d32940346 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ tmp coverage test/log test_db -test_db-journal \ No newline at end of file +test_db-journal +.idea \ No newline at end of file diff --git a/lib/jsonapi/configuration.rb b/lib/jsonapi/configuration.rb index 586b959fb..8ae8e8ec0 100644 --- a/lib/jsonapi/configuration.rb +++ b/lib/jsonapi/configuration.rb @@ -25,7 +25,8 @@ class Configuration :exception_class_whitelist, :always_include_to_one_linkage_data, :always_include_to_many_linkage_data, - :cache_formatters + :cache_formatters, + :validate_includes def initialize #:underscored_key, :camelized_key, :dasherized_key, or custom @@ -44,6 +45,7 @@ def initialize self.allow_include = true self.allow_sort = true self.allow_filter = true + self.validate_includes = true self.raise_if_parameters_not_allowed = true @@ -153,7 +155,7 @@ def exception_class_whitelisted?(e) @exception_class_whitelist.flatten.any? { |k| e.class.ancestors.include?(k) } end - attr_writer :allow_include, :allow_sort, :allow_filter + attr_writer :allow_include, :allow_sort, :allow_filter, :validate_includes attr_writer :default_paginator diff --git a/lib/jsonapi/request.rb b/lib/jsonapi/request.rb index 7977cd91f..cee7f1eb7 100644 --- a/lib/jsonapi/request.rb +++ b/lib/jsonapi/request.rb @@ -211,7 +211,7 @@ def check_include(resource_klass, include_parts) def parse_include_directives(include) return if include.nil? - + validate_includes = JSONAPI.configuration.validate_includes unless JSONAPI.configuration.allow_include fail JSONAPI::Exceptions::ParametersNotAllowed.new([:include]) end @@ -221,7 +221,7 @@ def parse_include_directives(include) include = [] included_resources.each do |included_resource| - check_include(@resource_klass, included_resource.partition('.')) + check_include(@resource_klass, included_resource.partition('.')) if validate_includes include.push(unformat_key(included_resource).to_s) end diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index fe83ab950..3e14fadde 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -502,18 +502,19 @@ def resolve_relationship_names_to_relations(resource_klass, model_includes, opti when Array return model_includes.map do |value| resolve_relationship_names_to_relations(resource_klass, value, options) - end + end.compact when Hash model_includes.keys.each do |key| relationship = resource_klass._relationships[key] value = model_includes[key] model_includes.delete(key) - model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options) + relationship_names = resolve_relationship_names_to_relations(relationship.resource_klass, value, options) + model_includes[relationship.relation_name(options)] = relationship_names unless relationship_names.nil? end return model_includes when Symbol relationship = resource_klass._relationships[model_includes] - return relationship.relation_name(options) + return relationship.relation_name(options) unless relationship.nil? end end @@ -521,7 +522,7 @@ def apply_includes(records, options = {}) include_directives = options[:include_directives] if include_directives model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options) - records = records.includes(model_includes) + records = records.includes(model_includes) unless model_includes.nil? || model_includes.empty? end records diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index 6898a38dc..31c666734 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -1862,6 +1862,15 @@ def test_expense_entries_show_bad_include_missing_relationship assert_match /employees is not a valid relationship of expenseEntries/, json_response['errors'][1]['detail'] end + def test_expense_entries_show_bad_include_missing_relationship_ignored + JSONAPI.configuration.validate_includes = false + get :show, params: {id: 1, include: 'isoCurrencies,employees'} + assert_response :success + assert json_response['data'].is_a?(Hash) + ensure + JSONAPI.configuration.validate_includes = true + end + def test_expense_entries_show_bad_include_missing_sub_relationship get :show, params: {id: 1, include: 'isoCurrency,employee.post'} assert_response :bad_request diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index bce4e444f..3e649b58a 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -993,6 +993,14 @@ def test_include_parameter_not_allowed JSONAPI.configuration.allow_include = true end + def test_include_parameter_not_validated + JSONAPI.configuration.validate_includes = false + get '/api/v2/books/1/book_comments?include=author' + assert_jsonapi_response 200 + ensure + JSONAPI.configuration.validate_includes = true + end + def test_filter_parameter_not_allowed JSONAPI.configuration.allow_filter = false get '/api/v2/books?filter[author]=1' From 77923a8f989c8595bd1c75fd5054d84e589f35c2 Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Mon, 9 May 2016 12:28:41 +0100 Subject: [PATCH 03/11] Introduced associated_resources_for(relationship_name) to the resource When adding new relationships on a class, these new methods are now used in place of the code being defined inline (to allow re use in the next phase) Note - the same code is being executed - just in a slightly different way - no functionality changes or breaking changes, with the exception of if someone has these methods already defined --- lib/jsonapi/associated_resources.rb | 114 ++++++++++++++++++++++++++++ lib/jsonapi/resource.rb | 82 ++++++-------------- lib/jsonapi/resource_serializer.rb | 7 +- 3 files changed, 140 insertions(+), 63 deletions(-) create mode 100644 lib/jsonapi/associated_resources.rb diff --git a/lib/jsonapi/associated_resources.rb b/lib/jsonapi/associated_resources.rb new file mode 100644 index 000000000..9d89f17f9 --- /dev/null +++ b/lib/jsonapi/associated_resources.rb @@ -0,0 +1,114 @@ +module JSONAPI + class AssociatedResources + class << self + def associated_resources_for(resource, relationship_name, options = {}) + relationship = resource._relationships[relationship_name] + associated_records_method_name = case relationship + when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}" + when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}" + end + if relationship.is_a?(JSONAPI::Relationship::ToOne) + if relationship.belongs_to? + _associated_resource_for_belongs_to(resource, relationship, associated_records_method_name, options) + else + _associated_resource_for_has_one(resource, relationship, associated_records_method_name, options) + end + + elsif relationship.is_a?(JSONAPI::Relationship::ToMany) + _associated_resources_for_has_many(resource, relationship, associated_records_method_name, options) + end + + end + + def associated_foreign_keys_for(resource, relationship_name) + relationship = resource._relationships[relationship_name] + foreign_key = relationship.foreign_key + associated_records_method_name = case relationship + when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}" + when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}" + end + if relationship.is_a?(JSONAPI::Relationship::ToOne) + if relationship.belongs_to? + _associated_foreign_key_for_belongs_to(resource, relationship, associated_records_method_name) + else + _associated_foreign_key_for_has_one(resource, relationship, associated_records_method_name) + end + elsif relationship.is_a?(JSONAPI::Relationship::ToMany) + _associated_foreign_keys_for_has_many(resource, relationship, associated_records_method_name) + end + end + + private + + def _associated_foreign_key_for_belongs_to(resource, relationship, associated_records_method_name) + resource._model.respond_to?(relationship.foreign_key) ? resource._model.method(relationship.foreign_key).call : resource.associated_foreign_keys_for(relationship.name.to_sym) + end + + def _associated_foreign_key_for_has_one(resource, relationship, associated_records_method_name) + record = resource.respond_to?(associated_records_method_name) ? resource.public_send(associated_records_method_name) : resource.associated_records_for(relationship.name.to_sym) + return nil if record.nil? + record.public_send(relationship.resource_klass._primary_key) + end + + def _associated_foreign_keys_for_has_many(resource, relationship, associated_records_method_name) + records = resource.respond_to?(associated_records_method_name) ? resource.public_send(associated_records_method_name) : resource.associated_records_for(relationship.name.to_sym) + return records.collect do |record| + record.public_send(relationship.resource_klass._primary_key) + end + end + + def _associated_resource_for_belongs_to(resource, relationship, associated_records_method_name = nil, options = {}) + if relationship.polymorphic? + associated_model = associated_records_method_name.nil? ? resource.associated_records_for(relationship_name) : resource.public_send(associated_records_method_name) + resource_klass = resource.class.resource_for_model(associated_model) if associated_model + return resource_klass.new(associated_model, @context) if resource_klass + else + resource_klass = relationship.resource_klass + if resource_klass + associated_model = resource.respond_to?(associated_records_method_name) ? resource.public_send(associated_records_method_name) : resource.associated_records_for(relationship_name) + return associated_model ? resource_klass.new(associated_model, @context) : nil + end + end + + end + + def _associated_resource_for_has_one(resource, relationship, associated_records_method_name = nil, options = {}) + resource_klass = relationship.resource_klass + if resource_klass + associated_model = resource.respond_to?(associated_records_method_name) ? resource.public_send(associated_records_method_name) : resource.associated_records_for(relationship_name) + return associated_model ? resource_klass.new(associated_model, @context) : nil + end + end + + def _associated_resources_for_has_many(resource, relationship, associated_records_method_name = nil, options = {}) + resource_klass = relationship.resource_klass + records = resource.respond_to?(associated_records_method_name) ? resource.public_send(associated_records_method_name) : resource.associated_records_for(relationship.name.to_sym) + + filters = options.fetch(:filters, {}) + unless filters.nil? || filters.empty? + records = resource_klass.apply_filters(records, filters, options) + end + + sort_criteria = options.fetch(:sort_criteria, {}) + unless sort_criteria.nil? || sort_criteria.empty? + order_options = relationship.resource_klass.construct_order_options(sort_criteria) + records = resource_klass.apply_sort(records, order_options, @context) + end + + paginator = options[:paginator] + if paginator + records = resource_klass.apply_pagination(records, paginator, order_options) + end + + return records.collect do |record| + if relationship.polymorphic? + resource_klass = resource.class.resource_for_model(record) + end + resource_klass.new(record, @context) + end + end + + end + + end +end \ No newline at end of file diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 3e14fadde..75889390c 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -1,4 +1,5 @@ require 'jsonapi/callbacks' +require 'jsonapi/associated_resources' module JSONAPI class Resource @@ -165,6 +166,17 @@ def _relationships self.class._relationships end + def associated_records_for(relationship_name) + relationship = _relationships[relationship_name] + relation_name = relationship.relation_name(context: @context) + records_for(relation_name) + end + + def associated_foreign_keys_for(relationship_name) + relationship = _relationships[relationship_name] + _model.send(relationship.foreign_key) + end + private def save @@ -312,6 +324,8 @@ def _replace_fields(field_data) :completed end + + class << self def inherited(subclass) subclass.abstract(false) @@ -880,87 +894,35 @@ def _add_relationship(klass, *attrs) end unless method_defined?("#{foreign_key}=") define_method associated_records_method_name do - relationship = _relationships[relationship_name] - relation_name = relationship.relation_name(context: @context) - records_for(relation_name) + associated_records_for(relationship_name) end unless method_defined?(associated_records_method_name) if relationship.is_a?(JSONAPI::Relationship::ToOne) if relationship.belongs_to? define_method foreign_key do - @model.method(foreign_key).call + AssociatedResources.associated_foreign_keys_for(self, relationship_name) end unless method_defined?(foreign_key) define_method relationship_name do |options = {}| - relationship = _relationships[relationship_name] - - if relationship.polymorphic? - associated_model = public_send(associated_records_method_name) - resource_klass = self.class.resource_for_model(associated_model) if associated_model - return resource_klass.new(associated_model, @context) if resource_klass - else - resource_klass = relationship.resource_klass - if resource_klass - associated_model = public_send(associated_records_method_name) - return associated_model ? resource_klass.new(associated_model, @context) : nil - end - end + AssociatedResources.associated_resources_for(self, relationship_name, options) + end unless method_defined?(relationship_name) else define_method foreign_key do - relationship = _relationships[relationship_name] - - record = public_send(associated_records_method_name) - return nil if record.nil? - record.public_send(relationship.resource_klass._primary_key) + AssociatedResources.associated_foreign_keys_for(self, relationship_name) end unless method_defined?(foreign_key) define_method relationship_name do |options = {}| - relationship = _relationships[relationship_name] - - resource_klass = relationship.resource_klass - if resource_klass - associated_model = public_send(associated_records_method_name) - return associated_model ? resource_klass.new(associated_model, @context) : nil - end + AssociatedResources.associated_resources_for(self, relationship_name, options) end unless method_defined?(relationship_name) end elsif relationship.is_a?(JSONAPI::Relationship::ToMany) define_method foreign_key do - records = public_send(associated_records_method_name) - return records.collect do |record| - record.public_send(relationship.resource_klass._primary_key) - end + AssociatedResources.associated_foreign_keys_for(self, relationship_name) end unless method_defined?(foreign_key) define_method relationship_name do |options = {}| - relationship = _relationships[relationship_name] - - resource_klass = relationship.resource_klass - records = public_send(associated_records_method_name) - - filters = options.fetch(:filters, {}) - unless filters.nil? || filters.empty? - records = resource_klass.apply_filters(records, filters, options) - end - - sort_criteria = options.fetch(:sort_criteria, {}) - unless sort_criteria.nil? || sort_criteria.empty? - order_options = relationship.resource_klass.construct_order_options(sort_criteria) - records = resource_klass.apply_sort(records, order_options, @context) - end - - paginator = options[:paginator] - if paginator - records = resource_klass.apply_pagination(records, paginator, order_options) - end - - return records.collect do |record| - if relationship.polymorphic? - resource_klass = self.class.resource_for_model(record) - end - resource_klass.new(record, @context) - end + AssociatedResources.associated_resources_for(self, relationship_name, options) end unless method_defined?(relationship_name) end end diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index 2aa0a74c1..b1427c7af 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -1,3 +1,4 @@ +require 'jsonapi/associated_resources' module JSONAPI class ResourceSerializer @@ -206,7 +207,7 @@ def relationships_hash(source, include_directives) include_linkage = ia && ia[:include] include_linked_children = ia && !ia[:include_related].empty? - resources = (include_linkage || include_linked_children) && [source.public_send(name)].flatten.compact + resources = (include_linkage || include_linked_children) && [AssociatedResources.associated_resources_for(source, name)].flatten.compact if field_set.include?(name) hash[format_key(name)] = link_object(source, relationship, include_linkage) @@ -305,7 +306,7 @@ def foreign_key_value(source, relationship) def foreign_key_types_and_values(source, relationship) if relationship.is_a?(JSONAPI::Relationship::ToMany) if relationship.polymorphic? - assoc = source._model.public_send(relationship.name) + assoc = source._model.respond_to?(relationship.name) ? source._model.public_send(relationship.name) : source.records_for(relationship.name) # Avoid hitting the database again for values already pre-loaded if assoc.respond_to?(:loaded?) and assoc.loaded? assoc.map do |obj| @@ -317,7 +318,7 @@ def foreign_key_types_and_values(source, relationship) end end else - source.public_send(relationship.foreign_key).map do |value| + AssociatedResources.associated_foreign_keys_for(source, relationship.name.to_sym).map do |value| [relationship.type, @id_formatter.format(value)] end end From ca71dc953ba282984c51f100defb4c870dd0c0e8 Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Wed, 11 May 2016 11:14:42 +0100 Subject: [PATCH 04/11] Changed as the save method may not be defined - but the instance may still respond to :save which is the important thing. --- lib/jsonapi/resource.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 75889390c..5992812e5 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -203,7 +203,7 @@ def _save(validation_context = nil) fail JSONAPI::Exceptions::ValidationErrors.new(self) end - if defined? @model.save + if @model.respond_to?(:save) saved = @model.save(validate: false) unless saved if @model.errors.present? From 015eaa24173e98d2995c37845f57cd8ed992246a Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Sat, 14 May 2016 14:33:06 +0100 Subject: [PATCH 05/11] The resolve_relationship_names_to_relations method now filters out any relations that are not real AR relations that can be passed to AR's include method. This resolves the issue where a 'relationship' is purely a method on an AR object that might return an array, an AR scope etc.. but is not a true relationship. When the query is executed the relationship will not exist and AR gets confused --- lib/jsonapi/resource.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 5992812e5..ad0f8a1f7 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -528,7 +528,10 @@ def resolve_relationship_names_to_relations(resource_klass, model_includes, opti return model_includes when Symbol relationship = resource_klass._relationships[model_includes] - return relationship.relation_name(options) unless relationship.nil? + return nil if relationship.nil? + relationship_name = relationship.relation_name(options) + return nil if resource_klass._model_class.respond_to?(:reflect_on_association) && resource_klass._model_class.reflect_on_association(relationship_name).nil? + relationship_name end end From 552af3cf37d78476b24645a0a96282701096dc8f Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Mon, 16 May 2016 07:53:22 +0100 Subject: [PATCH 06/11] Updated request test to include json api media type --- test/integration/requests/request_test.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index 02f293d42..6a3c8e87d 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -1079,7 +1079,9 @@ def test_include_parameter_not_allowed def test_include_parameter_not_validated JSONAPI.configuration.validate_includes = false - get '/api/v2/books/1/book_comments?include=author' + get '/api/v2/books/1/book_comments?include=author', headers: { + 'Accept' => JSONAPI::MEDIA_TYPE + } assert_jsonapi_response 200 ensure JSONAPI.configuration.validate_includes = true From 7febce549641148c26208a8ac520af95563c217f Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Thu, 19 May 2016 09:18:44 +0100 Subject: [PATCH 07/11] Updated request test to include json api media type --- lib/jsonapi/resource.rb | 59 ++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index ad0f8a1f7..baa7a39f2 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -309,10 +309,10 @@ def _replace_fields(field_data) remove_to_one_link(relationship_type) else case value - when Hash - replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type)) - else - replace_to_one_link(relationship_type, value) + when Hash + replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type)) + else + replace_to_one_link(relationship_type, value) end end end if field_data[:to_one] @@ -439,7 +439,7 @@ def relationship(*attrs) else #:nocov:# fail ArgumentError.new('to: must be either :one or :many') - #:nocov:# + #:nocov:# end _add_relationship(klass, *attrs, options.except(:to)) end @@ -522,8 +522,11 @@ def resolve_relationship_names_to_relations(resource_klass, model_includes, opti relationship = resource_klass._relationships[key] value = model_includes[key] model_includes.delete(key) - relationship_names = resolve_relationship_names_to_relations(relationship.resource_klass, value, options) - model_includes[relationship.relation_name(options)] = relationship_names unless relationship_names.nil? + relationship_name = relationship.relation_name(options) + if resource_klass._model_class.respond_to?(:reflect_on_association) && resource_klass._model_class.reflect_on_association(relationship_name).present? + relationship_names = resolve_relationship_names_to_relations(relationship.resource_klass, value, options) + model_includes[relationship_name] = relationship_names unless relationship_names.nil? + end end return model_includes when Symbol @@ -552,7 +555,7 @@ def apply_pagination(records, paginator, order_options) def apply_sort(records, order_options, _context = {}) if order_options.any? - order_options.each_pair do |field, direction| + order_options.each_pair do |field, direction| if field.to_s.include?(".") *model_names, column_name = field.split(".") @@ -732,25 +735,25 @@ def verify_key(key, context = nil) key_type = resource_key_type case key_type - when :integer - return if key.nil? - Integer(key) - when :string - return if key.nil? - if key.to_s.include?(',') - raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) - else - key - end - when :uuid - return if key.nil? - if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/) - key + when :integer + return if key.nil? + Integer(key) + when :string + return if key.nil? + if key.to_s.include?(',') + raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) + else + key + end + when :uuid + return if key.nil? + if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/) + key + else + raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) + end else - raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) - end - else - key_type.call(key, context) + key_type.call(key, context) end rescue raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) @@ -886,8 +889,8 @@ def _add_relationship(klass, *attrs) @_relationships[relationship_name] = relationship = klass.new(relationship_name, options) associated_records_method_name = case relationship - when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}" - when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}" + when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}" + when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}" end foreign_key = relationship.foreign_key From 31ddca6dee3a9bd677dab47e08fce384966cdbe6 Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Sat, 21 May 2016 18:03:03 +0100 Subject: [PATCH 08/11] non terminal relationships in the chain are excluded in resolve_relationship_names_to_relations when they are not defined as AR relationships. This prevents issues when the query with its includes is executed --- lib/jsonapi/resource.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index baa7a39f2..9ad8c8bda 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -522,22 +522,24 @@ def resolve_relationship_names_to_relations(resource_klass, model_includes, opti relationship = resource_klass._relationships[key] value = model_includes[key] model_includes.delete(key) - relationship_name = relationship.relation_name(options) - if resource_klass._model_class.respond_to?(:reflect_on_association) && resource_klass._model_class.reflect_on_association(relationship_name).present? - relationship_names = resolve_relationship_names_to_relations(relationship.resource_klass, value, options) - model_includes[relationship_name] = relationship_names unless relationship_names.nil? - end + parent_relation_name = relationship.relation_name(options) + next if _exclude_relationship(resource_klass, parent_relation_name) + relationship_names = resolve_relationship_names_to_relations(relationship.resource_klass, value, options) + model_includes[parent_relation_name] = relationship_names unless relationship_names.nil? end return model_includes when Symbol relationship = resource_klass._relationships[model_includes] return nil if relationship.nil? relationship_name = relationship.relation_name(options) - return nil if resource_klass._model_class.respond_to?(:reflect_on_association) && resource_klass._model_class.reflect_on_association(relationship_name).nil? + return nil if _exclude_relationship(resource_klass, relationship_name) relationship_name end end + def _exclude_relationship(resource_klass, relationship_name) + resource_klass._model_class.respond_to?(:reflect_on_association) && resource_klass._model_class.reflect_on_association(relationship_name).nil? + end def apply_includes(records, options = {}) include_directives = options[:include_directives] if include_directives From fbae9c4b7bfdcabbf26c3de8242d9e18a1f1cc12 Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Mon, 23 May 2016 10:42:26 +0100 Subject: [PATCH 09/11] find_by_key now uses overridable find_scope method --- lib/jsonapi/resource.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index baa7a39f2..6d052987a 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -673,7 +673,7 @@ def find(filters, options = {}) def find_by_key(key, options = {}) context = options[:context] - records = records(options) + records = find_scope(options) records = apply_includes(records, options) model = records.where({_primary_key => key}).first fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil? @@ -686,6 +686,13 @@ def records(_options = {}) _model_class.all end + # Override this method if you want the find scope to not use the collection scope (records) + # useful if the records scope is particularly expensive and you do not really care if the + # root scope is used instead (i.e. the AR model) + def find_scope(options) + records(options) + end + def verify_filters(filters, context = nil) verified_filters = {} filters.each do |filter, raw_value| From 1de901013f1c6b483650c1456384c434d65f1221 Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Thu, 2 Jun 2016 12:15:23 +0100 Subject: [PATCH 10/11] Merge branch 'master' of https://github.com/cerebris/jsonapi-resources into cerebris-master # Conflicts: # lib/jsonapi/configuration.rb --- lib/jsonapi/configuration.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/jsonapi/configuration.rb b/lib/jsonapi/configuration.rb index 16e6ac47a..77c6ff0f4 100644 --- a/lib/jsonapi/configuration.rb +++ b/lib/jsonapi/configuration.rb @@ -1,6 +1,5 @@ require 'jsonapi/formatter' -require 'jsonapi/operations_processor' -require 'jsonapi/active_record_operations_processor' +require 'jsonapi/processor' require 'concurrent' module JSONAPI From 8aa380b912b7d630e255c22e0d8ddb5c3b433ea4 Mon Sep 17 00:00:00 2001 From: Gary Taylor Date: Fri, 8 Jul 2016 13:59:34 +0100 Subject: [PATCH 11/11] Fields validation can now be disabled to allow for dynamic relationships Relationships that do not exist in the class can now still be included --- lib/jsonapi/configuration.rb | 6 ++++-- lib/jsonapi/request_parser.rb | 3 ++- lib/jsonapi/resource.rb | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/jsonapi/configuration.rb b/lib/jsonapi/configuration.rb index 77c6ff0f4..4d9b8ae01 100644 --- a/lib/jsonapi/configuration.rb +++ b/lib/jsonapi/configuration.rb @@ -25,7 +25,8 @@ class Configuration :always_include_to_one_linkage_data, :always_include_to_many_linkage_data, :cache_formatters, - :validate_includes + :validate_includes, + :validate_fields def initialize #:underscored_key, :camelized_key, :dasherized_key, or custom @@ -42,6 +43,7 @@ def initialize self.allow_sort = true self.allow_filter = true self.validate_includes = true + self.validate_fields = true self.raise_if_parameters_not_allowed = true @@ -154,7 +156,7 @@ def default_processor_klass=(default_processor_klass) @default_processor_klass = default_processor_klass end - attr_writer :allow_include, :allow_sort, :allow_filter, :validate_includes + attr_writer :allow_include, :allow_sort, :allow_filter, :validate_includes, :validate_fields attr_writer :default_paginator diff --git a/lib/jsonapi/request_parser.rb b/lib/jsonapi/request_parser.rb index cf782e68b..c319450ec 100644 --- a/lib/jsonapi/request_parser.rb +++ b/lib/jsonapi/request_parser.rb @@ -146,6 +146,7 @@ def parse_pagination(page) def parse_fields(fields) return if fields.nil? + validate_fields = JSONAPI.configuration.validate_fields extracted_fields = {} @@ -180,7 +181,7 @@ def parse_fields(fields) unless values.nil? valid_fields = type_resource.fields.collect { |key| format_key(key) } values.each do |field| - if valid_fields.include?(field) + if !validate_fields || valid_fields.include?(field) extracted_fields[type].push unformat_key(field) else @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, field).errors) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 22d832f36..6b58879a4 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -530,6 +530,7 @@ def resolve_relationship_names_to_relations(resource_klass, model_includes, opti relationship = resource_klass._relationships[key] value = model_includes[key] model_includes.delete(key) + next if relationship.nil? parent_relation_name = relationship.relation_name(options) next if _exclude_relationship(resource_klass, parent_relation_name) relationship_names = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)