lib/jsonapi/resource.rb in jsonapi-resources-0.7.1.beta1 vs lib/jsonapi/resource.rb in jsonapi-resources-0.7.1.beta2

- old
+ new

@@ -150,10 +150,19 @@ # the _options hash will contain the serializer and the serialization_options def meta(_options) {} end + # Override this to return custom links + # must return a hash, which will be merged with the default { self: 'self-url' } links hash + # links keys will be not be formatted with the key formatter for the serializer by default. + # They can however use the serializer's format_key and format_value methods if desired + # the _options hash will contain the serializer and the serialization_options + def custom_links(_options) + {} + end + private def save run_callbacks :save do _save @@ -171,12 +180,12 @@ # def _save # super # return :accepted # end # ``` - def _save - unless @model.valid? + def _save(validation_context = nil) + unless @model.valid?(validation_context) fail JSONAPI::Exceptions::ValidationErrors.new(self) end if defined? @model.save saved = @model.save(validate: false) @@ -199,10 +208,13 @@ def _remove unless @model.destroy fail JSONAPI::Exceptions::ValidationErrors.new(self) end :completed + + rescue ActiveRecord::DeleteRestrictionError => e + fail JSONAPI::Exceptions::RecordLocked.new(e.message) end def _create_to_many_links(relationship_type, relationship_key_values) relationship = self.class._relationships[relationship_type] @@ -254,10 +266,15 @@ relation_name = self.class._relationships[relationship_type].relation_name(context: @context) @model.public_send(relation_name).delete(key) :completed + + rescue ActiveRecord::DeleteRestrictionError => e + fail JSONAPI::Exceptions::RecordLocked.new(e.message) + rescue ActiveRecord::RecordNotFound + fail JSONAPI::Exceptions::RecordNotFound.new(key) end def _remove_to_one_link(relationship_type) relationship = self.class._relationships[relationship_type] @@ -388,15 +405,15 @@ end @_attributes ||= {} @_attributes[attr] = options define_method attr do - @model.public_send(attr) + @model.public_send(options[:delegate] ? options[:delegate].to_sym : attr) end unless method_defined?(attr) define_method "#{attr}=" do |value| - @model.public_send "#{attr}=", value + @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value) end unless method_defined?("#{attr}=") end def default_attribute_options { format: :default } @@ -519,21 +536,61 @@ records end def apply_sort(records, order_options, _context = {}) if order_options.any? - records.order(order_options) - else - records + order_options.each_pair do |field, direction| + if field.to_s.include?(".") + *model_names, column_name = field.split(".") + + associations = _lookup_association_chain([records.model.to_s, *model_names]) + joins_query = _build_joins([records.model, *associations]) + + # _sorting is appended to avoid name clashes with manual joins eg. overriden filters + order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}" + records = records.joins(joins_query).order(order_by_query) + else + records = records.order(field => direction) + end + end end + + records end + def _lookup_association_chain(model_names) + associations = [] + model_names.inject do |prev, current| + association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc| + assoc.name.to_s.downcase == current.downcase + end + associations << association + association.class_name + end + + associations + end + + def _build_joins(associations) + joins = [] + + associations.inject do |prev, current| + joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}" + current + end + joins.join("\n") + end + def apply_filter(records, filter, value, options = {}) strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply] if strategy - strategy.call(records, value, options) + if strategy.is_a?(Symbol) || strategy.is_a?(String) + send(strategy, records, value, options) + else + strategy.call(records, value, options) + end else records.where(filter => value) end end @@ -569,12 +626,17 @@ def sort_records(records, order_options, context = {}) apply_sort(records, order_options, context) end + # Assumes ActiveRecord's counting. Override if you need a different counting method + def count_records(records) + records.count(:all) + end + def find_count(filters, options = {}) - filter_records(filters, options).count(:all) + count_records(filter_records(filters, options)) end # Override this method if you have more complex requirements than this basic find method provides def find(filters, options = {}) context = options[:context] @@ -585,13 +647,19 @@ order_options = construct_order_options(sort_criteria) records = sort_records(records, order_options, context) records = apply_pagination(records, options[:paginator], order_options) + resources_for(records, context) + end + + def resources_for(records, context) resources = [] + resource_classes = {} records.each do |model| - resources.push self.resource_for_model(model).new(model, context) + resource_class = resource_classes[model.class] ||= self.resource_for_model(model) + resources.push resource_class.new(model, context) end resources end @@ -623,16 +691,23 @@ filter == _type || _relationships.include?(filter) end def verify_filter(filter, raw, context = nil) filter_values = [] - filter_values += CSV.parse_line(raw) unless raw.nil? || raw.empty? + if raw.present? + filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw] + end strategy = _allowed_filters.fetch(filter, Hash.new)[:verify] if strategy - [filter, strategy.call(filter_values, context)] + if strategy.is_a?(Symbol) || strategy.is_a?(String) + values = send(strategy, filter_values, context) + else + values = strategy.call(filter_values, context) + end + [filter, values] else if is_filter_relationship?(filter) verify_relationship_filter(filter, filter_values, context) else verify_custom_filter(filter, filter_values, context) @@ -643,10 +718,10 @@ def key_type(key_type) @_resource_key_type = key_type end def resource_key_type - @_resource_key_type || JSONAPI.configuration.resource_key_type + @_resource_key_type ||= JSONAPI.configuration.resource_key_type end def verify_key(key, context = nil) key_type = resource_key_type