From 601f8eb9aeb7d33be9e1f0954ea168aec1104dd5 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Wed, 22 Mar 2017 15:56:23 -0700 Subject: [PATCH 1/9] Move ActiveRecord logic to ActiveRecordAccessor --- lib/jsonapi/active_record_accessor.rb | 14 ++++++++++++++ lib/jsonapi/acts_as_resource_controller.rb | 14 ++------------ lib/jsonapi/record_accessor.rb | 19 +++++++++++++++++++ lib/jsonapi/resource.rb | 3 ++- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lib/jsonapi/active_record_accessor.rb b/lib/jsonapi/active_record_accessor.rb index 18ae2abaf..b9fff92e3 100644 --- a/lib/jsonapi/active_record_accessor.rb +++ b/lib/jsonapi/active_record_accessor.rb @@ -5,6 +5,20 @@ class ActiveRecordAccessor < RecordAccessor # RecordAccessor methods + def transaction + ActiveRecord::Base.transaction do + yield + end + end + + def rollback_transaction + fail ActiveRecord::Rollback + end + + def model_error_messages(model) + model.errors.messages + end + def find_resource(filters, options = {}) if options[:caching] && options[:caching][:cache_serializer_output] find_serialized_with_caching(filters, options[:caching][:serializer], options) diff --git a/lib/jsonapi/acts_as_resource_controller.rb b/lib/jsonapi/acts_as_resource_controller.rb index 830109df5..6180a6607 100644 --- a/lib/jsonapi/acts_as_resource_controller.rb +++ b/lib/jsonapi/acts_as_resource_controller.rb @@ -114,7 +114,7 @@ def process_request def run_in_transaction(transactional) if transactional run_callbacks :transaction do - transaction do + resource_klass._record_accessor.transaction do yield end end @@ -126,7 +126,7 @@ def run_in_transaction(transactional) def rollback_transaction(transactional) if transactional run_callbacks :rollback do - rollback + resource_klass._record_accessor.rollback_transaction end end end @@ -136,16 +136,6 @@ def process_operation(operation) response_document.add_result(result, operation) end - def transaction - ActiveRecord::Base.transaction do - yield - end - end - - def rollback - fail ActiveRecord::Rollback - end - private def resource_klass diff --git a/lib/jsonapi/record_accessor.rb b/lib/jsonapi/record_accessor.rb index 3cf39ee99..aa59efe14 100644 --- a/lib/jsonapi/record_accessor.rb +++ b/lib/jsonapi/record_accessor.rb @@ -6,6 +6,25 @@ def initialize(resource_klass) @_resource_klass = resource_klass end + def transaction + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + # Should return an enumerable with the key being the attribute name and value being an array of error messages. + def model_error_messages(model) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def rollback_transaction + # :nocov: + raise 'Abstract method called' + # :nocov: + end + # Resource records def find_resource(_filters, _options = {}) # :nocov: diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index a9667bb1e..e11fce91a 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -117,8 +117,9 @@ def fetchable_fields self.class.fields end + # Some or def model_error_messages - _model.errors.messages + self.class._record_accessor.model_error_messages(_model) end # Add metadata to validation error objects. From 92457fd9efe129fc1d81f4f281edc65551095012 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Thu, 23 Mar 2017 01:54:29 -0700 Subject: [PATCH 2/9] Refactor ActiveRecordAccessor location * ActiveRecordAccessor extends from RecordAccessor, for consistency it should be ActiveRecordRecordAccessor because other orms will be OrmRecordAccessor < RecordAccessor which makes more sense. * Another idea would be to rename RecordAccessor but then I think it would be difficult to understand what the Accessor class was for. * Separate out dependencies in the gemspec. The gem no longer requires ActiveRecord to be loaded into your namespace. --- .gitignore | 2 + jsonapi-resources.gemspec | 5 +- lib/jsonapi-resources.rb | 3 +- ...or.rb => active_record_record_accessor.rb} | 67 ++++++++++++------- lib/jsonapi/configuration.rb | 7 +- lib/jsonapi/record_accessor.rb | 23 +++++++ lib/jsonapi/resource.rb | 19 +++--- 7 files changed, 84 insertions(+), 42 deletions(-) rename lib/jsonapi/{active_record_accessor.rb => active_record_record_accessor.rb} (92%) diff --git a/.gitignore b/.gitignore index 6cc125d63..b15de95a8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,9 @@ .bundle .config .yardoc +.ruby-gemset .ruby-version +.byebug_history Gemfile.lock InstalledFiles _yardoc diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index 3f031d173..83ed527e5 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -25,8 +25,11 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'minitest-spec-rails' spec.add_development_dependency 'simplecov' spec.add_development_dependency 'pry' + spec.add_development_dependency 'pry-byebug' spec.add_development_dependency 'concurrent-ruby-ext' - spec.add_dependency 'activerecord', '>= 4.1' + spec.add_development_dependency 'sequel' + spec.add_development_dependency 'activerecord', '>= 4.1' + spec.add_dependency 'activesupport', '>= 4.1' spec.add_dependency 'railties', '>= 4.1' spec.add_dependency 'concurrent-ruby' end diff --git a/lib/jsonapi-resources.rb b/lib/jsonapi-resources.rb index e16022ff6..aedecdcf5 100644 --- a/lib/jsonapi-resources.rb +++ b/lib/jsonapi-resources.rb @@ -25,4 +25,5 @@ require 'jsonapi/callbacks' require 'jsonapi/link_builder' require 'jsonapi/record_accessor' -require 'jsonapi/active_record_accessor' +require 'jsonapi/active_record_record_accessor' +require 'jsonapi/sequel_record_accessor' \ No newline at end of file diff --git a/lib/jsonapi/active_record_accessor.rb b/lib/jsonapi/active_record_record_accessor.rb similarity index 92% rename from lib/jsonapi/active_record_accessor.rb rename to lib/jsonapi/active_record_record_accessor.rb index b9fff92e3..f31664910 100644 --- a/lib/jsonapi/active_record_accessor.rb +++ b/lib/jsonapi/active_record_record_accessor.rb @@ -1,8 +1,7 @@ require 'jsonapi/record_accessor' module JSONAPI - class ActiveRecordAccessor < RecordAccessor - + class ActiveRecordRecordAccessor < RecordAccessor # RecordAccessor methods def transaction @@ -11,6 +10,18 @@ def transaction end end + def model_base_class + ActiveRecord::Base + end + + def delete_restriction_error_class + ActiveRecord::DeleteRestrictionError + end + + def record_not_found_error_class + ActiveRecord::RecordNotFound + end + def rollback_transaction fail ActiveRecord::Rollback end @@ -19,6 +30,10 @@ def model_error_messages(model) model.errors.messages end + def association_model_class_name(from_model, relationship_name) + from_model.reflect_on_association(relationship_name) + end + def find_resource(filters, options = {}) if options[:caching] && options[:caching][:cache_serializer_output] find_serialized_with_caching(filters, options[:caching][:serializer], options) @@ -292,21 +307,21 @@ def count_records(records) def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {}) case model_includes - when Array - return model_includes.map do |value| - resolve_relationship_names_to_relations(resource_klass, value, options) - end - 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) - end - return model_includes - when Symbol - relationship = resource_klass._relationships[model_includes] - return relationship.relation_name(options) + when Array + return model_includes.map do |value| + resolve_relationship_names_to_relations(resource_klass, value, options) + end + 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) + end + return model_includes + when Symbol + relationship = resource_klass._relationships[model_includes] + return relationship.relation_name(options) end end @@ -402,8 +417,8 @@ def preload_included_fragments(resources, records, serializer, options) pluck_attrs << _resource_klass._model_class.arel_table[_resource_klass._primary_key] relation = records - .except(:limit, :offset, :order) - .where({ _resource_klass._primary_key => res_ids }) + .except(:limit, :offset, :order) + .where({ _resource_klass._primary_key => res_ids }) # These are updated as we iterate through the association path; afterwards they will # refer to the final resource on the path, i.e. the actual resource to find in the cache. @@ -469,17 +484,17 @@ def preload_included_fragments(resources, records, serializer, options) if klass.caching? sub_cache_ids = id_rows - .map { |row| row.last(2) } - .reject { |row| target_resources[klass.name].has_key?(row.first) } - .uniq + .map { |row| row.last(2) } + .reject { |row| target_resources[klass.name].has_key?(row.first) } + .uniq target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments( - klass, serializer, context, sub_cache_ids + klass, serializer, context, sub_cache_ids ) else sub_res_ids = id_rows - .map(&:last) - .reject { |id| target_resources[klass.name].has_key?(id) } - .uniq + .map(&:last) + .reject { |id| target_resources[klass.name].has_key?(id) } + .uniq found = klass.find({ klass._primary_key => sub_res_ids }, context: options[:context]) target_resources[klass.name].merge! found.map { |r| [r.id, r] }.to_h end diff --git a/lib/jsonapi/configuration.rb b/lib/jsonapi/configuration.rb index 52c34ab81..15b8edc6a 100644 --- a/lib/jsonapi/configuration.rb +++ b/lib/jsonapi/configuration.rb @@ -1,7 +1,6 @@ require 'jsonapi/formatter' require 'jsonapi/processor' -require 'jsonapi/record_accessor' -require 'jsonapi/active_record_accessor' +require 'jsonapi/active_record_record_accessor' require 'concurrent' module JSONAPI @@ -96,10 +95,10 @@ def initialize self.always_include_to_many_linkage_data = false # Record Accessor - # The default Record Accessor is the ActiveRecordAccessor which provides + # The default Record Accessor is the JSONAPI::ActiveRecordRecordAccessor which provides # caching access to ActiveRecord backed models. Custom Accessors can be specified # in order to support other models. - self.default_record_accessor_klass = JSONAPI::ActiveRecordAccessor + self.default_record_accessor_klass = JSONAPI::ActiveRecordRecordAccessor # The default Operation Processor to use if one is not defined specifically # for a Resource. diff --git a/lib/jsonapi/record_accessor.rb b/lib/jsonapi/record_accessor.rb index aa59efe14..7d4e9b6ae 100644 --- a/lib/jsonapi/record_accessor.rb +++ b/lib/jsonapi/record_accessor.rb @@ -2,10 +2,33 @@ module JSONAPI class RecordAccessor attr_reader :_resource_klass + # Note: model_base_class, delete_restriction_error_class, record_not_found_error_class could be defined as + # class attributes but currently all the library files are loaded using 'require', so if we have something like + # self.model_base_class = ActiveRecord::Base, then ActiveRecord would be required as a dependency. Leaving these + # as instance methods means we can load in these files at load-time and use them if they so choose. + def initialize(resource_klass) @_resource_klass = resource_klass end + def model_base_class + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def delete_restriction_error_class + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def record_not_found_error_class + # :nocov: + raise 'Abstract method called' + # :nocov: + end + def transaction # :nocov: raise 'Abstract method called' diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index e11fce91a..c32fd2816 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -224,8 +224,8 @@ def _remove fail JSONAPI::Exceptions::ValidationErrors.new(self) end :completed - - rescue ActiveRecord::DeleteRestrictionError => e + self.class._record_accessor.model_error_messages(_model) + rescue self.class._record_accessor.delete_restriction_error_class => e fail JSONAPI::Exceptions::RecordLocked.new(e.message) end @@ -355,9 +355,9 @@ def _remove_to_many_link(relationship_type, key, options) :completed - rescue ActiveRecord::DeleteRestrictionError => e + rescue self.class._record_accessor.delete_restriction_error_class => e fail JSONAPI::Exceptions::RecordLocked.new(e.message) - rescue ActiveRecord::RecordNotFound + rescue self.class._record_accessor.record_not_found_error_class fail JSONAPI::Exceptions::RecordNotFound.new(key) end @@ -914,12 +914,11 @@ def _add_relationship(klass, *attrs) # ResourceBuilder methods def define_relationship_methods(relationship_name, relationship_klass, options) - # Initialize from an ActiveRecord model's properties - if _model_class && _model_class.ancestors.collect { |ancestor| ancestor.name }.include?('ActiveRecord::Base') - model_association = _model_class.reflect_on_association(relationship_name) - if model_association - options = options.reverse_merge(class_name: model_association.class_name) - end + # Initialize from an ORM model's properties + if _model_class.is_a?(_record_accessor.model_base_class) && + (association_model_class_name = association_model_class_name(_model_class, relationship_name)) + + options = options.reverse_merge(class_name: association_model_class_name) end relationship = register_relationship( From ac2ed31326abd85293af97d1dfda276b1da290be Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Thu, 23 Mar 2017 01:57:30 -0700 Subject: [PATCH 3/9] Add in skeleton for SequelRecordAccessor --- lib/jsonapi/sequel_record_accessor.rb | 523 ++++++++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 lib/jsonapi/sequel_record_accessor.rb diff --git a/lib/jsonapi/sequel_record_accessor.rb b/lib/jsonapi/sequel_record_accessor.rb new file mode 100644 index 000000000..0465907ea --- /dev/null +++ b/lib/jsonapi/sequel_record_accessor.rb @@ -0,0 +1,523 @@ +require 'jsonapi/record_accessor' + +module JSONAPI + class SequelRecordAccessor < RecordAccessor + + def transaction + ::Sequel.transaction(::Sequel::DATABASES) do + yield + end + end + + def rollback_transaction + fail ::Sequel::Rollback + end + + def model_error_messages(model) + model.errors + end + + def model_base_class + Sequel + end + + def delete_restriction_error_class + ActiveRecord::DeleteRestrictionError + end + + def record_not_found_error_class + ActiveRecord::RecordNotFound + end + + def find_resource(filters, options = {}) + if options[:caching] && options[:caching][:cache_serializer_output] + find_serialized_with_caching(filters, options[:caching][:serializer], options) + else + _resource_klass.resources_for(find_records(filters, options), options[:context]) + end + end + + def find_resource_by_key(key, options = {}) + if options[:caching] && options[:caching][:cache_serializer_output] + find_by_key_serialized_with_caching(key, options[:caching][:serializer], options) + else + records = find_records({ _resource_klass._primary_key => key }, options.except(:paginator, :sort_criteria)) + model = records.first + fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil? + _resource_klass.resource_for(model, options[:context]) + end + end + + def find_resources_by_keys(keys, options = {}) + records = records(options) + records = apply_includes(records, options) + records = records.where({ _resource_klass._primary_key => keys }) + + _resource_klass.resources_for(records, options[:context]) + end + + def find_count(filters, options = {}) + count_records(filter_records(filters, options)) + end + + def related_resource(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + if relationship.polymorphic? + associated_model = records_for_relationship(resource, relationship_name, options) + resource_klass = resource.class.resource_klass_for_model(associated_model) if associated_model + return resource_klass.new(associated_model, resource.context) if resource_klass && associated_model + else + resource_klass = relationship.resource_klass + if resource_klass + associated_model = records_for_relationship(resource, relationship_name, options) + return associated_model ? resource_klass.new(associated_model, resource.context) : nil + end + end + end + + def related_resources(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + relationship_resource_klass = relationship.resource_klass + + if options[:caching] && options[:caching][:cache_serializer_output] + scope = relationship_resource_klass._record_accessor.records_for_relationship(resource, relationship_name, options) + relationship_resource_klass._record_accessor.find_serialized_with_caching(scope, options[:caching][:serializer], options) + else + records = records_for_relationship(resource, relationship_name, options) + return records.collect do |record| + klass = relationship.polymorphic? ? resource.class.resource_klass_for_model(record) : relationship_resource_klass + klass.new(record, resource.context) + end + end + end + + def count_for_relationship(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + context = resource.context + + relation_name = relationship.relation_name(context: context) + records = records_for(resource, relation_name) + + resource_klass = relationship.resource_klass + + filters = options.fetch(:filters, {}) + unless filters.nil? || filters.empty? + records = resource_klass._record_accessor.apply_filters(records, filters, options) + end + + records.count(:all) + end + + def foreign_key(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + if relationship.belongs_to? + resource._model.method(relationship.foreign_key).call + else + records = records_for_relationship(resource, relationship_name, options) + return nil if records.nil? + records.public_send(relationship.resource_klass._primary_key) + end + end + + def foreign_keys(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + records = records_for_relationship(resource, relationship_name, options) + records.collect do |record| + record.public_send(relationship.resource_klass._primary_key) + end + end + + # protected-ish methods left public for tests and what not + + def find_serialized_with_caching(filters_or_source, serializer, options = {}) + if filters_or_source.is_a?(ActiveRecord::Relation) + return cached_resources_for(filters_or_source, serializer, options) + elsif _resource_klass._model_class.respond_to?(:all) && _resource_klass._model_class.respond_to?(:arel_table) + records = find_records(filters_or_source, options.except(:include_directives)) + return cached_resources_for(records, serializer, options) + else + # :nocov: + warn('Caching enabled on model that does not support ActiveRelation') + # :nocov: + end + end + + def find_by_key_serialized_with_caching(key, serializer, options = {}) + if _resource_klass._model_class.respond_to?(:all) && _resource_klass._model_class.respond_to?(:arel_table) + results = find_serialized_with_caching({ _resource_klass._primary_key => key }, serializer, options) + result = results.first + fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil? + return result + else + # :nocov: + warn('Caching enabled on model that does not support ActiveRelation') + # :nocov: + end + end + + def records_for_relationship(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + context = resource.context + + relation_name = relationship.relation_name(context: context) + records = records_for(resource, relation_name) + + resource_klass = relationship.resource_klass + + filters = options.fetch(:filters, {}) + unless filters.nil? || filters.empty? + records = resource_klass._record_accessor.apply_filters(records, filters, options) + end + + sort_criteria = options.fetch(:sort_criteria, {}) + order_options = relationship.resource_klass.construct_order_options(sort_criteria) + records = apply_sort(records, order_options, context) + + paginator = options[:paginator] + if paginator + records = apply_pagination(records, paginator, order_options) + end + + records + end + + # Implement self.records on the resource if you want to customize the relation for + # finder methods (find, find_by_key, find_serialized_with_caching) + def records(_options = {}) + if defined?(_resource_klass.records) + _resource_klass.records(_options) + else + _resource_klass._model_class.all + end + end + + # Implement records_for on the resource to customize how the associated records + # are fetched for a model. Particularly helpful for authorization. + def records_for(resource, relation_name) + if resource.respond_to?(:records_for) + return resource.records_for(relation_name) + end + + relationship = resource.class._relationships[relation_name] + + if relationship.is_a?(JSONAPI::Relationship::ToMany) + if resource.respond_to?(:"records_for_#{relation_name}") + return resource.method(:"records_for_#{relation_name}").call + end + else + if resource.respond_to?(:"record_for_#{relation_name}") + return resource.method(:"record_for_#{relation_name}").call + end + end + + resource._model.public_send(relation_name) + end + + def apply_includes(records, options = {}) + include_directives = options[:include_directives] + if include_directives + model_includes = resolve_relationship_names_to_relations(_resource_klass, include_directives.model_includes, options) + records = records.includes(model_includes) + end + + records + end + + def apply_pagination(records, paginator, order_options) + records = paginator.apply(records, order_options) if paginator + records + end + + def apply_sort(records, order_options, context = {}) + if defined?(_resource_klass.apply_sort) + _resource_klass.apply_sort(records, order_options, context) + else + if order_options.any? + 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. overridden 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 + 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 = _resource_klass._allowed_filters.fetch(filter.to_sym, Hash.new)[:apply] + + if strategy + if strategy.is_a?(Symbol) || strategy.is_a?(String) + _resource_klass.send(strategy, records, value, options) + else + strategy.call(records, value, options) + end + else + records.where(filter => value) + end + end + + # Assumes ActiveRecord's counting. Override if you need a different counting method + def count_records(records) + records.count(:all) + end + + def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {}) + case model_includes + when Array + return model_includes.map do |value| + resolve_relationship_names_to_relations(resource_klass, value, options) + end + 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) + end + return model_includes + when Symbol + relationship = resource_klass._relationships[model_includes] + return relationship.relation_name(options) + end + end + + def apply_filters(records, filters, options = {}) + required_includes = [] + + if filters + filters.each do |filter, value| + if _resource_klass._relationships.include?(filter) + if _resource_klass._relationships[filter].belongs_to? + records = apply_filter(records, _resource_klass._relationships[filter].foreign_key, value, options) + else + required_includes.push(filter.to_s) + records = apply_filter(records, "#{_resource_klass._relationships[filter].table_name}.#{_resource_klass._relationships[filter].primary_key}", value, options) + end + else + records = apply_filter(records, filter, value, options) + end + end + end + + if required_includes.any? + records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(_resource_klass, required_includes, force_eager_load: true))) + end + + records + end + + def filter_records(filters, options, records = records(options)) + records = apply_filters(records, filters, options) + apply_includes(records, options) + end + + def sort_records(records, order_options, context = {}) + apply_sort(records, order_options, context) + end + + def cached_resources_for(records, serializer, options) + if _resource_klass.caching? + t = _resource_klass._model_class.arel_table + cache_ids = pluck_arel_attributes(records, t[_resource_klass._primary_key], t[_resource_klass._cache_field]) + resources = CachedResourceFragment.fetch_fragments(_resource_klass, serializer, options[:context], cache_ids) + else + resources = _resource_klass.resources_for(records, options[:context]).map { |r| [r.id, r] }.to_h + end + + preload_included_fragments(resources, records, serializer, options) + + resources.values + end + + def find_records(filters, options = {}) + if defined?(_resource_klass.find_records) + ActiveSupport::Deprecation.warn "In #{_resource_klass.name} you overrode `find_records`. "\ + "`find_records` has been deprecated in favor of using `apply` "\ + "and `verify` callables on the filter." + + _resource_klass.find_records(filters, options) + else + context = options[:context] + + records = filter_records(filters, options) + + sort_criteria = options.fetch(:sort_criteria) { [] } + order_options = _resource_klass.construct_order_options(sort_criteria) + records = sort_records(records, order_options, context) + + records = apply_pagination(records, options[:paginator], order_options) + + records + end + end + + def preload_included_fragments(resources, records, serializer, options) + return if resources.empty? + res_ids = resources.keys + + include_directives = options[:include_directives] + return unless include_directives + + context = options[:context] + + # For each association, including indirect associations, find the target record ids. + # Even if a target class doesn't have caching enabled, we still have to look up + # and match the target ids here, because we can't use ActiveRecord#includes. + # + # Note that `paths` returns partial paths before complete paths, so e.g. the partial + # fragments for posts.comments will exist before we start working with posts.comments.author + target_resources = {} + include_directives.paths.each do |path| + # If path is [:posts, :comments, :author], then... + pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at] + pluck_attrs << _resource_klass._model_class.arel_table[_resource_klass._primary_key] + + relation = records + .except(:limit, :offset, :order) + .where({ _resource_klass._primary_key => res_ids }) + + # These are updated as we iterate through the association path; afterwards they will + # refer to the final resource on the path, i.e. the actual resource to find in the cache. + # So e.g. if path is [:posts, :comments, :author], then after iteration... + parent_klass = nil # Comment + klass = _resource_klass # Person + relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author + table = nil # people + assocs_path = [] # [ :posts, :approved_comments, :author ] + ar_hash = nil # { :posts => { :approved_comments => :author } } + + # For each step on the path, figure out what the actual table name/alias in the join + # will be, and include the primary key of that table in our list of fields to select + non_polymorphic = true + path.each do |elem| + relationship = klass._relationships[elem] + if relationship.polymorphic + # Can't preload through a polymorphic belongs_to association, ResourceSerializer + # will just have to bypass the cache and load the real Resource. + non_polymorphic = false + break + end + assocs_path << relationship.relation_name(options).to_sym + # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }} + ar_hash = assocs_path.reverse.reduce { |memo, step| { step => memo } } + # We can't just look up the table name from the resource class, because Arel could + # have used a table alias if the relation includes a self-reference. + join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node| + arel_node.is_a?(Arel::Nodes::InnerJoin) + end + table = join_source.left + parent_klass = klass + klass = relationship.resource_klass + pluck_attrs << table[klass._primary_key] + end + next unless non_polymorphic + + # Pre-fill empty hashes for each resource up to the end of the path. + # This allows us to later distinguish between a preload that returned nothing + # vs. a preload that never ran. + prefilling_resources = resources.values + path.each do |rel_name| + rel_name = serializer.key_formatter.format(rel_name) + prefilling_resources.map! do |res| + res.preloaded_fragments[rel_name] ||= {} + res.preloaded_fragments[rel_name].values + end + prefilling_resources.flatten!(1) + end + + pluck_attrs << table[klass._cache_field] if klass.caching? + relation = relation.joins(ar_hash) + if relationship.is_a?(JSONAPI::Relationship::ToMany) + # Rails doesn't include order clauses in `joins`, so we have to add that manually here. + # FIXME Should find a better way to reflect on relationship ordering. :-( + relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders) + end + + # [[post id, comment id, author id, author updated_at], ...] + id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs) + + target_resources[klass.name] ||= {} + + if klass.caching? + sub_cache_ids = id_rows + .map { |row| row.last(2) } + .reject { |row| target_resources[klass.name].has_key?(row.first) } + .uniq + target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments( + klass, serializer, context, sub_cache_ids + ) + else + sub_res_ids = id_rows + .map(&:last) + .reject { |id| target_resources[klass.name].has_key?(id) } + .uniq + found = klass.find({ klass._primary_key => sub_res_ids }, context: options[:context]) + target_resources[klass.name].merge! found.map { |r| [r.id, r] }.to_h + end + + id_rows.each do |row| + res = resources[row.first] + path.each_with_index do |rel_name, index| + rel_name = serializer.key_formatter.format(rel_name) + rel_id = row[index+1] + assoc_rels = res.preloaded_fragments[rel_name] + if index == path.length - 1 + assoc_rels[rel_id] = target_resources[klass.name].fetch(rel_id) + else + res = assoc_rels[rel_id] + end + end + end + end + end + + def pluck_arel_attributes(relation, *attrs) + conn = relation.connection + quoted_attrs = attrs.map do |attr| + quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name) + quoted_column = conn.quote_column_name(attr.name) + "#{quoted_table}.#{quoted_column}" + end + relation.pluck(*quoted_attrs) + end + end +end \ No newline at end of file From b7e71d4b1d2837c0826eb0e55dd0bc2d246b1c1c Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Thu, 23 Mar 2017 02:50:48 -0700 Subject: [PATCH 4/9] Add sequel fixtures and models --- test/fixtures/sequel.rb | 1877 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1877 insertions(+) create mode 100644 test/fixtures/sequel.rb diff --git a/test/fixtures/sequel.rb b/test/fixtures/sequel.rb new file mode 100644 index 000000000..62326f732 --- /dev/null +++ b/test/fixtures/sequel.rb @@ -0,0 +1,1877 @@ +require 'active_record' +require 'jsonapi-resources' + +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.uncountable 'preferences' + inflect.irregular 'numero_telefone', 'numeros_telefone' +end + +### DATABASE +ActiveRecord::Schema.define do + create_table :people, force: true do |t| + t.string :name + t.string :email + t.datetime :date_joined + t.belongs_to :preferences + t.integer :hair_cut_id, index: true + t.boolean :book_admin, default: false + t.boolean :special, default: false + t.timestamps null: false + end + + create_table :author_details, force: true do |t| + t.integer :person_id + t.string :author_stuff + end + + create_table :posts, force: true do |t| + t.string :title, length: 255 + t.text :body + t.integer :author_id + t.integer :parent_post_id + t.belongs_to :section, index: true + t.timestamps null: false + end + + create_table :comments, force: true do |t| + t.text :body + t.belongs_to :post, index: true + t.integer :author_id + t.timestamps null: false + end + + create_table :companies, force: true do |t| + t.string :type + t.string :name + t.string :address + t.timestamps null: false + end + + create_table :tags, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :sections, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :posts_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :posts_tags, [:post_id, :tag_id], unique: true + + create_table :special_post_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :special_post_tags, [:post_id, :tag_id], unique: true + + create_table :comments_tags, force: true do |t| + t.references :comment, :tag, index: true + end + + create_table :iso_currencies, id: false, force: true do |t| + t.string :code, limit: 3, null: false + t.string :name + t.string :country_name + t.string :minor_unit + t.timestamps null: false + end + add_index :iso_currencies, :code, unique: true + + create_table :expense_entries, force: true do |t| + t.string :currency_code, limit: 3, null: false + t.integer :employee_id, null: false + t.decimal :cost, precision: 12, scale: 4, null: false + t.date :transaction_date + t.timestamps null: false + end + + create_table :planets, force: true do |t| + t.string :name + t.string :description + t.integer :planet_type_id + end + + create_table :planets_tags, force: true do |t| + t.references :planet, :tag, index: true + end + add_index :planets_tags, [:planet_id, :tag_id], unique: true + + create_table :planet_types, force: true do |t| + t.string :name + end + + create_table :moons, force: true do |t| + t.string :name + t.string :description + t.integer :planet_id + t.timestamps null: false + end + + create_table :craters, id: false, force: true do |t| + t.string :code + t.string :description + t.integer :moon_id + t.timestamps null: false + end + + create_table :preferences, force: true do |t| + t.integer :person_id + t.boolean :advanced_mode, default: false + t.timestamps null: false + end + + create_table :facts, force: true do |t| + t.integer :person_id + t.string :spouse_name + t.text :bio + t.float :quality_rating + t.decimal :salary, precision: 12, scale: 2 + t.datetime :date_time_joined + t.date :birthday + t.time :bedtime + t.binary :photo, limit: 1.kilobyte + t.boolean :cool + t.timestamps null: false + end + + create_table :books, force: true do |t| + t.string :title + t.string :isbn + t.boolean :banned, default: false + t.timestamps null: false + end + + create_table :book_authors, force: true do |t| + t.integer :book_id + t.integer :person_id + end + + create_table :book_comments, force: true do |t| + t.text :body + t.belongs_to :book, index: true + t.integer :author_id + t.boolean :approved, default: true + t.timestamps null: false + end + + create_table :customers, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :purchase_orders, force: true do |t| + t.date :order_date + t.date :requested_delivery_date + t.date :delivery_date + t.integer :customer_id + t.string :delivery_name + t.string :delivery_address_1 + t.string :delivery_address_2 + t.string :delivery_city + t.string :delivery_state + t.string :delivery_postal_code + t.float :delivery_fee + t.float :tax + t.float :total + t.timestamps null: false + end + + create_table :order_flags, force: true do |t| + t.string :name + end + + create_table :purchase_orders_order_flags, force: true do |t| + t.references :purchase_order, :order_flag, index: true + end + add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" + + create_table :line_items, force: true do |t| + t.integer :purchase_order_id + t.string :part_number + t.string :quantity + t.float :item_cost + t.timestamps null: false + end + + create_table :hair_cuts, force: true do |t| + t.string :style + end + + create_table :numeros_telefone, force: true do |t| + t.string :numero_telefone + t.timestamps null: false + end + + create_table :categories, force: true do |t| + t.string :name + t.string :status, limit: 10 + t.timestamps null: false + end + + create_table :pictures, force: true do |t| + t.string :name + t.integer :imageable_id + t.string :imageable_type + t.timestamps null: false + end + + create_table :documents, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :products, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :vehicles, force: true do |t| + t.string :type + t.string :make + t.string :model + t.string :length_at_water_line + t.string :drive_layout + t.string :serial_number + t.integer :person_id + t.timestamps null: false + end + + create_table :makes, force: true do |t| + t.string :model + t.timestamps null: false + end + + # special cases - fields that look like they should be reserved names + create_table :hrefs, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :links, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :web_pages, force: true do |t| + t.string :href + t.string :link + t.timestamps null: false + end + + create_table :questionables, force: true do |t| + t.timestamps null: false + end + + create_table :boxes, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :things, force: true do |t| + t.string :name + t.references :user + t.references :box + + t.timestamps null: false + end + + create_table :users, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :related_things, force: true do |t| + t.string :name + t.references :from, references: :thing + t.references :to, references: :thing + + t.timestamps null: false + end + + create_table :questions, force: true do |t| + t.string :text + end + + create_table :answers, force: true do |t| + t.references :question + t.integer :respondent_id + t.string :respondent_type + t.string :text + end + + create_table :patients, force: true do |t| + t.string :name + end + + create_table :doctors, force: true do |t| + t.string :name + end + + # special cases +end + +### MODELS +class Person < ActiveRecord::Base + has_many :posts, foreign_key: 'author_id' + has_many :comments, foreign_key: 'author_id' + has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception + has_many :vehicles + belongs_to :preferences + belongs_to :hair_cut + has_one :author_detail + + has_and_belongs_to_many :books, join_table: :book_authors + + has_many :even_posts, -> { where('posts.id % 2 = 0') }, class_name: 'Post', foreign_key: 'author_id' + has_many :odd_posts, -> { where('posts.id % 2 = 1') }, class_name: 'Post', foreign_key: 'author_id' + + ### Validations + validates :name, presence: true + validates :date_joined, presence: true +end + +class AuthorDetail < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'person_id' +end + +class Post < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' + has_many :comments + has_and_belongs_to_many :tags, join_table: :posts_tags + has_many :special_post_tags, source: :tag + has_many :special_tags, through: :special_post_tags, source: :tag + belongs_to :section + has_one :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' + + validates :author, presence: true + validates :title, length: { maximum: 35 } + + before_destroy :destroy_callback + + def destroy_callback + if title == "can't destroy me" + errors.add(:title, "can't destroy me") + + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class SpecialPostTag < ActiveRecord::Base + belongs_to :tag + belongs_to :post +end + +class Comment < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :post + has_and_belongs_to_many :tags, join_table: :comments_tags +end + +class Company < ActiveRecord::Base +end + +class Firm < Company +end + +class Tag < ActiveRecord::Base + has_and_belongs_to_many :posts, join_table: :posts_tags + has_and_belongs_to_many :planets, join_table: :planets_tags +end + +class Section < ActiveRecord::Base + has_many :posts +end + +class HairCut < ActiveRecord::Base + has_many :people +end + +class Property < ActiveRecord::Base +end + +class Customer < ActiveRecord::Base +end + +class BadlyNamedAttributes < ActiveRecord::Base +end + +class Cat < ActiveRecord::Base +end + +class IsoCurrency < ActiveRecord::Base + self.primary_key = :code + # has_many :expense_entries, foreign_key: 'currency_code' +end + +class ExpenseEntry < ActiveRecord::Base + belongs_to :employee, class_name: 'Person', foreign_key: 'employee_id' + belongs_to :iso_currency, foreign_key: 'currency_code' +end + +class Planet < ActiveRecord::Base + has_many :moons + belongs_to :planet_type + + has_and_belongs_to_many :tags, join_table: :planets_tags + + # Test model callback cancelling save + before_save :check_not_pluto + + def check_not_pluto + # Pluto can't be a planet, so cancel the save + if name.downcase == 'pluto' + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class PlanetType < ActiveRecord::Base + has_many :planets +end + +class Moon < ActiveRecord::Base + belongs_to :planet + + has_many :craters +end + +class Crater < ActiveRecord::Base + self.primary_key = :code + + belongs_to :moon +end + +class Preferences < ActiveRecord::Base + has_one :author, class_name: 'Person', :inverse_of => 'preferences' +end + +class Fact < ActiveRecord::Base + validates :spouse_name, :bio, presence: true +end + +class Like < ActiveRecord::Base +end + +class Breed + + def initialize(id = nil, name = nil) + if id.nil? + @id = $breed_data.new_id + $breed_data.add(self) + else + @id = id + end + @name = name + @errors = ActiveModel::Errors.new(self) + end + + attr_accessor :id, :name + + def destroy + $breed_data.remove(@id) + end + + def valid?(context = nil) + @errors.clear + if name.is_a?(String) && name.length > 0 + return true + else + @errors.add(:name, "can't be blank") + return false + end + end + + def errors + @errors + end +end + +class Book < ActiveRecord::Base + has_many :book_comments + has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" + + has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" +end + +class BookComment < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :book + + def self.for_user(current_user) + records = self + # Hide the unapproved comments from people who are not book admins + unless current_user && current_user.book_admin + records = records.where(approved: true) + end + records + end +end + +class BreedData + def initialize + @breeds = {} + end + + def breeds + @breeds + end + + def new_id + @breeds.keys.max + 1 + end + + def add(breed) + @breeds[breed.id] = breed + end + + def remove(id) + @breeds.delete(id) + end +end + +class Customer < ActiveRecord::Base + has_many :purchase_orders +end + +class PurchaseOrder < ActiveRecord::Base + belongs_to :customer + has_many :line_items + has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id' + + has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags + + has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag' +end + +class OrderFlag < ActiveRecord::Base + has_and_belongs_to_many :purchase_orders, join_table: :purchase_orders_order_flags +end + +class LineItem < ActiveRecord::Base + belongs_to :purchase_order +end + +class NumeroTelefone < ActiveRecord::Base +end + +class Category < ActiveRecord::Base +end + +class Picture < ActiveRecord::Base + belongs_to :imageable, polymorphic: true +end + +class Vehicle < ActiveRecord::Base + belongs_to :person +end + +class Car < Vehicle +end + +class Boat < Vehicle +end + +class Document < ActiveRecord::Base + has_many :pictures, as: :imageable +end + +class Document::Topic < Document +end + +class Product < ActiveRecord::Base + has_one :picture, as: :imageable +end + +class Make < ActiveRecord::Base +end + +class WebPage < ActiveRecord::Base +end + +class Box < ActiveRecord::Base + has_many :things +end + +class User < ActiveRecord::Base + has_many :things +end + +class Thing < ActiveRecord::Base + belongs_to :box + belongs_to :user + + has_many :related_things, foreign_key: :from_id + has_many :things, through: :related_things, source: :to +end + +class RelatedThing < ActiveRecord::Base + belongs_to :from, class_name: Thing, foreign_key: :from_id + belongs_to :to, class_name: Thing, foreign_key: :to_id +end + +class Question < ActiveRecord::Base + has_one :answer + + def respondent + answer.try(:respondent) + end +end + +class Answer < ActiveRecord::Base + belongs_to :question + belongs_to :respondent, polymorphic: true +end + +class Patient < ActiveRecord::Base +end + +class Doctor < ActiveRecord::Base +end + +module Api + module V7 + class Client < Customer + end + + class Customer < Customer + end + end +end + +### CONTROLLERS +class AuthorsController < JSONAPI::ResourceControllerMetal +end + +class PeopleController < JSONAPI::ResourceController +end + +class BaseController < ActionController::Base + include JSONAPI::ActsAsResourceController +end + +class PostsController < BaseController + + class SpecialError < StandardError; end + class SubSpecialError < PostsController::SpecialError; end + class SerializeError < StandardError; end + + # This is used to test that classes that are whitelisted are reraised by + # the operations dispatcher. + rescue_from PostsController::SpecialError do + head :forbidden + end + + #called by test_on_server_error + def self.set_callback_message(error) + @callback_message = "Sent from method" + end + + def resource_serializer_klass + PostSerializer + end +end + +class PostSerializer < JSONAPI::ResourceSerializer + def initialize(*) + if $PostSerializerRaisesErrors + raise PostsController::SerializeError + else + super + end + end +end + +class CommentsController < JSONAPI::ResourceController +end + +class FirmsController < JSONAPI::ResourceController +end + +class SectionsController < JSONAPI::ResourceController +end + +class TagsController < JSONAPI::ResourceController +end + +class IsoCurrenciesController < JSONAPI::ResourceController +end + +class ExpenseEntriesController < JSONAPI::ResourceController +end + +class BreedsController < JSONAPI::ResourceController +end + +class FactsController < JSONAPI::ResourceController +end + +class CategoriesController < JSONAPI::ResourceController +end + +class PicturesController < JSONAPI::ResourceController +end + +class DocumentsController < JSONAPI::ResourceController +end + +class ProductsController < JSONAPI::ResourceController +end + +class ImageablesController < JSONAPI::ResourceController +end + +class VehiclesController < JSONAPI::ResourceController +end + +class CarsController < JSONAPI::ResourceController +end + +class BoatsController < JSONAPI::ResourceController +end + +class BooksController < JSONAPI::ResourceController + def context + { title: 'Title' } + end +end + +### CONTROLLERS +module Api + module V1 + class AuthorsController < JSONAPI::ResourceController + end + + class PeopleController < JSONAPI::ResourceController + end + + class PostsController < ActionController::Base + include JSONAPI::ActsAsResourceController + end + + class TagsController < JSONAPI::ResourceController + end + + class IsoCurrenciesController < JSONAPI::ResourceController + end + + class ExpenseEntriesController < JSONAPI::ResourceController + end + + class BreedsController < JSONAPI::ResourceController + end + + class PlanetsController < JSONAPI::ResourceController + end + + class PlanetTypesController < JSONAPI::ResourceController + end + + class MoonsController < JSONAPI::ResourceController + end + + class CratersController < JSONAPI::ResourceController + def context + {current_user: $test_user} + end + end + + class LikesController < JSONAPI::ResourceController + end + end + + module V2 + class AuthorsController < JSONAPI::ResourceController + end + + class PeopleController < JSONAPI::ResourceController + end + + class PostsController < JSONAPI::ResourceController + end + + class PreferencesController < JSONAPI::ResourceController + end + + class BooksController < JSONAPI::ResourceController + def context + {current_user: $test_user} + end + end + + class BookCommentsController < JSONAPI::ResourceController + def context + {current_user: $test_user} + end + end + end + + module V3 + class PostsController < JSONAPI::ResourceController + end + end + + module V4 + class PostsController < JSONAPI::ResourceController + end + + class ExpenseEntriesController < JSONAPI::ResourceController + end + + class IsoCurrenciesController < JSONAPI::ResourceController + end + + class BooksController < JSONAPI::ResourceController + end + end + + module V5 + class AuthorsController < JSONAPI::ResourceController + def serialization_options + {foo: 'bar'} + end + end + + class PostsController < JSONAPI::ResourceController + end + + class ExpenseEntriesController < JSONAPI::ResourceController + end + + class IsoCurrenciesController < JSONAPI::ResourceController + end + end + + module V6 + class PostsController < JSONAPI::ResourceController + end + + class SectionsController < JSONAPI::ResourceController + end + + class CustomersController < JSONAPI::ResourceController + end + + class PurchaseOrdersController < JSONAPI::ResourceController + def context + {current_user: $test_user} + end + end + + class LineItemsController < JSONAPI::ResourceController + end + + class OrderFlagsController < JSONAPI::ResourceController + end + end + + module V7 + class CustomersController < JSONAPI::ResourceController + end + + class PurchaseOrdersController < JSONAPI::ResourceController + end + + class LineItemsController < JSONAPI::ResourceController + end + + class OrderFlagsController < JSONAPI::ResourceController + end + + class CategoriesController < JSONAPI::ResourceController + end + + class ClientsController < JSONAPI::ResourceController + end + end + + module V8 + class NumerosTelefoneController < JSONAPI::ResourceController + end + end +end + +module Api + class BoxesController < JSONAPI::ResourceController + end +end + +class QuestionsController < JSONAPI::ResourceController +end + +class AnswersController < JSONAPI::ResourceController +end + +class PatientsController < JSONAPI::ResourceController +end + +class DoctorsController < JSONAPI::ResourceController +end + +class RespondentController < JSONAPI::ResourceController +end + +### RESOURCES +class BaseResource < JSONAPI::Resource + abstract +end + +class PersonResource < BaseResource + attributes :name, :email + attribute :date_joined, format: :date_with_timezone + + has_many :comments, :posts + has_many :vehicles, polymorphic: true + + has_one :preferences + has_one :hair_cut + + filter :name, verify: :verify_name_filter + + def self.verify_name_filter(values, _context) + values.each do |value| + if value.length < 3 + raise JSONAPI::Exceptions::InvalidFilterValue.new(:name, value) + end + end + return values + end + +end + +class PersonWithEvenAndOddPostsResource < JSONAPI::Resource + model_name 'Person' + + has_many :even_posts, foreign_key: 'author_id', class_name: 'Post', relation_name: :even_posts + has_many :odd_posts, foreign_key: 'author_id', class_name: 'Post', relation_name: :odd_posts +end + +class SpecialBaseResource < BaseResource + abstract + + model_hint model: Person, resource: :special_person +end + +class SpecialPersonResource < SpecialBaseResource + model_name 'Person' + + def self.records(options = {}) + Person.where(special: true) + end +end + +class VehicleResource < JSONAPI::Resource + immutable + + has_one :person + attributes :make, :model, :serial_number +end + +class CarResource < VehicleResource + attributes :drive_layout +end + +class BoatResource < VehicleResource + attributes :length_at_water_line +end + +class CommentResource < JSONAPI::Resource + attributes :body + has_one :post + has_one :author, class_name: 'Person' + has_many :tags + + filters :body +end + +class CompanyResource < JSONAPI::Resource + attributes :name, :address +end + +class FirmResource < CompanyResource + model_name "Firm" +end + +class TagResource < JSONAPI::Resource + attributes :name + + has_many :posts + # Not including the planets relationship so they don't get output + #has_many :planets +end + +class SectionResource < JSONAPI::Resource + attributes 'name' +end + +module ParentApi + class PostResource < JSONAPI::Resource + model_name 'Post' + attributes :title + has_one :parent_post + end +end + +class PostResource < JSONAPI::Resource + attribute :title + attribute :body + attribute :subject + + has_one :author, class_name: 'Person' + has_one :section + has_many :tags, acts_as_set: true, inverse_relationship: :posts, eager_load_on_include: false + has_many :comments, acts_as_set: false, inverse_relationship: :post + + # Not needed - just for testing + primary_key :id + + def self.default_sort + [{field: 'title', direction: :desc}, {field: 'id', direction: :desc}] + end + + before_save do + msg = "Before save" + end + + after_save do + msg = "After save" + end + + before_update do + msg = "Before update" + end + + after_update do + msg = "After update" + end + + before_replace_fields do + msg = "Before replace_fields" + end + + after_replace_fields do + msg = "After replace_fields" + end + + around_update :around_update_check + + def around_update_check + # do nothing + yield + # do nothing + end + + def subject + @model.title + end + + def title=(title) + @model.title = title + if title == 'BOOM' + raise 'The Server just tested going boom. If this was a real emergency you would be really dead right now.' + end + end + + filters :title, :author, :tags, :comments + filter :id, verify: ->(values, context) { + verify_keys(values, context) + return values + } + filter :ids, + verify: ->(values, context) { + verify_keys(values, context) + return values + }, + apply: -> (records, value, _options) { + records.where('id IN (?)', value) + } + + filter :search, + verify: ->(values, context) { + values.all?{|v| (v.is_a?(Hash) || v.is_a?(ActionController::Parameters)) } && values + }, + apply: -> (records, values, _options) { + records.where(title: values.first['title']) + } + + def self.updatable_fields(context) + super(context) - [:author, :subject] + end + + def self.creatable_fields(context) + super(context) - [:subject] + end + + def self.sortable_fields(context) + super(context) - [:id] + [:"author.name"] + end + + def self.verify_key(key, context = nil) + super(key) + raise JSONAPI::Exceptions::RecordNotFound.new(key) unless find_by_key(key, context: context) + return key + end +end + +class HairCutResource < JSONAPI::Resource + attribute :style + has_many :people +end + +class IsoCurrencyResource < JSONAPI::Resource + attributes :name, :country_name, :minor_unit + attribute :id, format: :id, readonly: false + + filter :country_name + + key_type :string +end + +class ExpenseEntryResource < JSONAPI::Resource + attributes :cost + attribute :transaction_date, format: :date + + has_one :iso_currency, foreign_key: 'currency_code' + has_one :employee, class_name: 'Person' +end + +class EmployeeResource < JSONAPI::Resource + attributes :name, :email + model_name 'Person' +end + +class BreedResource < JSONAPI::Resource + attribute :name, format: :title + + # This is unneeded, just here for testing + routing_options param: :id + + def self.find(filters, options = {}) + breeds = [] + $breed_data.breeds.values.each do |breed| + breeds.push(BreedResource.new(breed, options[:context])) + end + breeds + end + + def self.find_by_key(id, options = {}) + BreedResource.new($breed_data.breeds[id.to_i], options[:context]) + end + + def _save + super + return :accepted + end +end + +class PlanetResource < JSONAPI::Resource + attribute :name + attribute :description + + has_many :moons + has_one :planet_type + + has_many :tags, acts_as_set: true +end + +class PropertyResource < JSONAPI::Resource + attributes :name + + has_many :planets +end + +class PlanetTypeResource < JSONAPI::Resource + attributes :name + has_many :planets, inverse_relationship: :planet_type +end + +class MoonResource < JSONAPI::Resource + attribute :name + attribute :description + + has_one :planet + has_many :craters +end + +class CraterResource < JSONAPI::Resource + attribute :code + attribute :description + + has_one :moon + + filter :description, apply: -> (records, value, options) { + fail "context not set" unless options[:context][:current_user] != nil && options[:context][:current_user] == $test_user + records.where(:description => value) + } + + def self.verify_key(key, context = nil) + key && String(key) + end +end + +class PreferencesResource < JSONAPI::Resource + attribute :advanced_mode + + has_one :author, :foreign_key_on => :related + + def self.find_records(filters, options = {}) + Preferences.limit(1) + end +end + +class FactResource < JSONAPI::Resource + attribute :spouse_name + attribute :bio + attribute :quality_rating + attribute :salary + attribute :date_time_joined + attribute :birthday + attribute :bedtime + attribute :photo + attribute :cool +end + +class CategoryResource < JSONAPI::Resource + filter :status, default: 'active' +end + +class PictureResource < JSONAPI::Resource + attribute :name + has_one :imageable, polymorphic: true +end + +class DocumentResource < JSONAPI::Resource + attribute :name + has_many :pictures +end + +class TopicResource < JSONAPI::Resource + model_name 'Document::Topic' + has_many :pictures +end + +class ProductResource < JSONAPI::Resource + attribute :name + has_one :picture, always_include_linkage_data: true + + def picture_id + _model.picture.id + end +end + +class ImageableResource < JSONAPI::Resource +end + +class MakeResource < JSONAPI::Resource + attribute :model +end + +class WebPageResource < JSONAPI::Resource + attribute :href + attribute :link +end + +class AuthorResource < JSONAPI::Resource + model_name 'Person' + attributes :name + + has_many :books, inverse_relationship: :authors +end + +class BookResource < JSONAPI::Resource + attribute :title + + has_many :authors, class_name: 'Author', inverse_relationship: :books + + def title + context[:title] + end +end + +class AuthorDetailResource < JSONAPI::Resource + attributes :author_stuff +end + +class SimpleCustomLinkResource < JSONAPI::Resource + model_name 'Post' + attributes :title, :body, :subject + + def subject + @model.title + end + + has_one :writer, foreign_key: 'author_id', class_name: 'Writer' + has_one :section + has_many :comments, acts_as_set: false + + filters :writer + + def custom_links(options) + { raw: options[:serializer].link_builder.self_link(self) + "/raw" } + end +end + +class CustomLinkWithRelativePathOptionResource < JSONAPI::Resource + model_name 'Post' + attributes :title, :body, :subject + + def subject + @model.title + end + + has_one :writer, foreign_key: 'author_id', class_name: 'Writer' + has_one :section + has_many :comments, acts_as_set: false + + filters :writer + + def custom_links(options) + { raw: options[:serializer].link_builder.self_link(self) + "/super/duper/path.xml" } + end +end + +class CustomLinkWithIfCondition < JSONAPI::Resource + model_name 'Post' + attributes :title, :body, :subject + + def subject + @model.title + end + + has_one :writer, foreign_key: 'author_id', class_name: 'Writer' + has_one :section + has_many :comments, acts_as_set: false + + filters :writer + + def custom_links(options) + if title == "JR Solves your serialization woes!" + {conditional_custom_link: options[:serializer].link_builder.self_link(self) + "/conditional/link.json"} + end + end +end + +class CustomLinkWithLambda < JSONAPI::Resource + model_name 'Post' + attributes :title, :body, :subject, :created_at + + def subject + @model.title + end + + has_one :writer, foreign_key: 'author_id', class_name: 'Writer' + has_one :section + has_many :comments, acts_as_set: false + + filters :writer + + def custom_links(options) + { + link_to_external_api: "http://external-api.com/posts/#{ created_at.year }/#{ created_at.month }/#{ created_at.day }-#{ subject.gsub(' ', '-') }" + } + end +end + +module Api + module V1 + class WriterResource < JSONAPI::Resource + attributes :name, :email + model_name 'Person' + has_many :posts + + filter :name + end + + class LikeResource < JSONAPI::Resource + end + + class PostResource < JSONAPI::Resource + # V1 no longer supports tags and now calls author 'writer' + attribute :title + attribute :body + attribute :subject + + has_one :writer, foreign_key: 'author_id', class_name: 'Writer' + has_one :section + has_many :comments, acts_as_set: false + + def self.default_sort + [{field: 'title', direction: :asc}, {field: 'id', direction: :desc}] + end + + def subject + @model.title + end + + filters :writer + end + + class PersonResource < PersonResource; end + class CommentResource < CommentResource; end + class TagResource < TagResource; end + class SectionResource < SectionResource; end + class IsoCurrencyResource < IsoCurrencyResource; end + class ExpenseEntryResource < ExpenseEntryResource; end + class BreedResource < BreedResource; end + class PlanetResource < PlanetResource; end + class PlanetTypeResource < PlanetTypeResource; end + class MoonResource < MoonResource; end + class CraterResource < CraterResource; end + class PreferencesResource < PreferencesResource; end + class EmployeeResource < EmployeeResource; end + class HairCutResource < HairCutResource; end + class VehicleResource < VehicleResource; end + class CarResource < CarResource; end + class BoatResource < BoatResource; end + end +end + +module Api + module V2 + class PreferencesResource < PreferencesResource; end + class PersonResource < PersonResource; end + class PostResource < PostResource; end + + class BookResource < JSONAPI::Resource + attribute :title + attributes :isbn, :banned + + has_many :authors + + has_many :book_comments, relation_name: -> (options = {}) { + context = options[:context] + current_user = context ? context[:current_user] : nil + + unless current_user && current_user.book_admin + :approved_book_comments + else + :book_comments + end + }, reflect: true + + has_many :aliased_comments, class_name: 'BookComments', relation_name: :approved_book_comments + + filters :book_comments + filter :banned, apply: :apply_filter_banned + + class << self + def books + Book.arel_table + end + + def not_banned_books + books[:banned].eq(false) + end + + def records(options = {}) + context = options[:context] + current_user = context ? context[:current_user] : nil + + records = _model_class + # Hide the banned books from people who are not book admins + unless current_user && current_user.book_admin + records = records.where(not_banned_books) + end + records + end + + def apply_filter_banned(records, value, options) + context = options[:context] + current_user = context ? context[:current_user] : nil + + # Only book admins might filter for banned books + if current_user && current_user.book_admin + records.where('books.banned = ?', value[0] == 'true') + end + end + + end + end + + class BookCommentResource < JSONAPI::Resource + attributes :body, :approved + + has_one :book + has_one :author, class_name: 'Person' + + filters :book + filter :approved, apply: ->(records, value, options) { + context = options[:context] + current_user = context ? context[:current_user] : nil + + if current_user && current_user.book_admin + records.where(approved_comments(value[0] == 'true')) + end + } + + class << self + def book_comments + BookComment.arel_table + end + + def approved_comments(approved = true) + book_comments[:approved].eq(approved) + end + + def records(options = {}) + current_user = options[:context][:current_user] + _model_class.for_user(current_user) + end + end + end + end +end + +module Api + module V3 + class PostResource < PostResource; end + class PreferencesResource < PreferencesResource; end + end +end + +module Api + module V4 + class PostResource < PostResource; end + class PersonResource < PersonResource; end + class ExpenseEntryResource < ExpenseEntryResource; end + class IsoCurrencyResource < IsoCurrencyResource; end + + class BookResource < Api::V2::BookResource + paginator :paged + end + + class BookCommentResource < Api::V2::BookCommentResource + paginator :paged + end + end +end + +module Api + module V5 + class AuthorResource < JSONAPI::Resource + attributes :name, :email + model_name 'Person' + relationship :posts, to: :many + relationship :author_detail, to: :one, foreign_key_on: :related + + filter :name + + def self.find_records(filters, options = {}) + rel = _model_class + filters.each do |attr, filter| + if attr.to_s == "id" + rel = rel.where(id: filter) + else + rel = rel.where("\"#{attr}\" LIKE \"%#{filter[0]}%\"") + end + end + rel + end + + def fetchable_fields + super - [:email] + end + end + + class AuthorDetailResource < JSONAPI::Resource + attributes :author_stuff + end + + class PersonResource < PersonResource; end + class PostResource < PostResource; end + class TagResource < TagResource; end + class SectionResource < SectionResource; end + class CommentResource < CommentResource; end + class ExpenseEntryResource < ExpenseEntryResource; end + class IsoCurrencyResource < IsoCurrencyResource; end + class EmployeeResource < EmployeeResource; end + end +end + +module Api + module V6 + class PersonResource < PersonResource; end + class TagResource < TagResource; end + + class SectionResource < SectionResource + has_many :posts + end + + class CommentResource < CommentResource; end + + class PostResource < PostResource + # Test caching with SQL fragments + def self.records(options = {}) + _model_class.all.joins('INNER JOIN people on people.id = author_id') + end + end + + class CustomerResource < JSONAPI::Resource + attribute :name + + has_many :purchase_orders + end + + class PurchaseOrderResource < JSONAPI::Resource + attribute :order_date + attribute :requested_delivery_date + attribute :delivery_date + attribute :delivery_name + attribute :delivery_address_1 + attribute :delivery_address_2 + attribute :delivery_city + attribute :delivery_state + attribute :delivery_postal_code + attribute :delivery_fee + attribute :tax + attribute :total + + has_one :customer + has_many :line_items, relation_name: -> (options = {}) { + context = options[:context] + current_user = context ? context[:current_user] : nil + + unless current_user && current_user.book_admin + :line_items + else + :admin_line_items + end + }, + reflect: false + + has_many :order_flags, acts_as_set: true, + relation_name: -> (options = {}) { + context = options[:context] + current_user = context ? context[:current_user] : nil + + unless current_user && current_user.book_admin + :order_flags + else + :admin_order_flags + end + } + end + + class OrderFlagResource < JSONAPI::Resource + attributes :name + + has_many :purchase_orders, reflect: false + end + + class LineItemResource < JSONAPI::Resource + attribute :part_number + attribute :quantity + attribute :item_cost + + has_one :purchase_order + end + end + + module V7 + class PurchaseOrderResource < V6::PurchaseOrderResource; end + class OrderFlagResource < V6::OrderFlagResource; end + class LineItemResource < V6::LineItemResource; end + + class CustomerResource < V6::CustomerResource + model_name 'Api::V7::Customer' + end + + class ClientResource < JSONAPI::Resource + model_name 'Api::V7::Customer' + + attribute :name + + has_many :purchase_orders + end + + class CategoryResource < CategoryResource + attribute :name + + # Raise exception for failure in controller + def name + fail "Something Exceptional Happened" + end + end + end + + module V8 + class NumeroTelefoneResource < JSONAPI::Resource + attribute :numero_telefone + end + end +end + +module AdminApi + module V1 + class PersonResource < JSONAPI::Resource + end + end +end + +module DasherizedNamespace + module V1 + class PersonResource < JSONAPI::Resource + end + end +end + +module MyEngine + module Api + module V1 + class PersonResource < JSONAPI::Resource + end + end + end + + module AdminApi + module V1 + class PersonResource < JSONAPI::Resource + end + end + end + + module DasherizedNamespace + module V1 + class PersonResource < JSONAPI::Resource + end + end + end +end + +module ApiV2Engine + class PersonResource < JSONAPI::Resource + end +end + +module Legacy + class FlatPost < ActiveRecord::Base + self.table_name = "posts" + end +end + +class FlatPostResource < JSONAPI::Resource + model_name "Legacy::FlatPost", add_model_hint: false + + model_hint model: "Legacy::FlatPost", resource: FlatPostResource + + attribute :title +end + +class FlatPostsController < JSONAPI::ResourceController +end + +# CustomProcessors +class Api::V4::BookProcessor < JSONAPI::Processor + after_find do + unless @results.is_a?(JSONAPI::ErrorsOperationResult) + @result.meta[:total_records] = @result.record_count + @result.links['spec'] = 'https://test_corp.com' + end + end +end + +class PostProcessor < JSONAPI::Processor + def find + if $PostProcessorRaisesErrors + raise PostsController::SubSpecialError + end + # puts("In custom Operations Processor without Namespace") + super + end + + after_find do + unless @results.is_a?(JSONAPI::ErrorsOperationResult) + @result.meta[:total_records] = @result.record_count + @result.links['spec'] = 'https://test_corp.com' + end + end +end + +module Api + module V7 + class CategoryProcessor < JSONAPI::Processor + def show + if $PostProcessorRaisesErrors + raise PostsController::SubSpecialError + end + # puts("In custom Operations Processor without Namespace") + super + end + end + end +end + +module Api + module V1 + class PostProcessor < JSONAPI::Processor + def show + # puts("In custom Operations Processor with Namespace") + super + end + end + end +end + +module Api + class BoxResource < JSONAPI::Resource + has_many :things + end + + class ThingResource < JSONAPI::Resource + has_one :box + has_one :user + + has_many :things + end + + class UserResource < JSONAPI::Resource + has_many :things + end +end + +class QuestionResource < JSONAPI::Resource + has_one :answer + has_one :respondent, polymorphic: true, class_name: "Respondent", foreign_key_on: :related + + attributes :text +end + +class AnswerResource < JSONAPI::Resource + has_one :question + has_one :respondent, polymorphic: true +end + +class PatientResource < JSONAPI::Resource + attributes :name +end + +class DoctorResource < JSONAPI::Resource + attributes :name +end + +class RespondentResource < JSONAPI::Resource + abstract +end + +### PORO Data - don't do this in a production app +$breed_data = BreedData.new +$breed_data.add(Breed.new(0, 'persian')) +$breed_data.add(Breed.new(1, 'siamese')) +$breed_data.add(Breed.new(2, 'sphinx')) +$breed_data.add(Breed.new(3, 'to_delete')) From ebcfcc31fc14c26d8e6a2fb04a26fc328cb45995 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Fri, 24 Mar 2017 17:49:35 -0700 Subject: [PATCH 5/9] Refactor tests to allow for setup and testing with multiple ORMs --- jsonapi-resources.gemspec | 1 + lib/jsonapi/active_record_record_accessor.rb | 18 +- lib/jsonapi/resource.rb | 10 +- lib/jsonapi/sequel_record_accessor.rb | 7 +- test/fixtures/sequel.rb | 1877 ----------------- .../controllers_resources_processors.rb} | 657 +----- test/support/models.rb | 350 +++ test/support/orm/active_record/initialize.rb | 1 + test/support/orm/active_record/models.rb | 354 ++++ test/support/orm/active_record/schema.rb | 310 +++ test/support/orm/active_record/setup.rb | 29 + test/support/orm/sequel/initialize.rb | 2 + test/support/orm/sequel/models.rb | 371 ++++ test/support/orm/sequel/setup.rb | 12 + test/support/orm/test_configurator.rb | 22 + test/test_helper.rb | 31 +- test/unit/resource/resource_test.rb | 3 + 17 files changed, 1489 insertions(+), 2566 deletions(-) delete mode 100644 test/fixtures/sequel.rb rename test/{fixtures/active_record.rb => support/controllers_resources_processors.rb} (65%) create mode 100644 test/support/models.rb create mode 100644 test/support/orm/active_record/initialize.rb create mode 100644 test/support/orm/active_record/models.rb create mode 100644 test/support/orm/active_record/schema.rb create mode 100644 test/support/orm/active_record/setup.rb create mode 100644 test/support/orm/sequel/initialize.rb create mode 100644 test/support/orm/sequel/models.rb create mode 100644 test/support/orm/sequel/setup.rb create mode 100644 test/support/orm/test_configurator.rb diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index 83ed527e5..44ed00afd 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -28,6 +28,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'pry-byebug' spec.add_development_dependency 'concurrent-ruby-ext' spec.add_development_dependency 'sequel' + spec.add_development_dependency 'sequel-rails' spec.add_development_dependency 'activerecord', '>= 4.1' spec.add_dependency 'activesupport', '>= 4.1' spec.add_dependency 'railties', '>= 4.1' diff --git a/lib/jsonapi/active_record_record_accessor.rb b/lib/jsonapi/active_record_record_accessor.rb index f31664910..8b710a444 100644 --- a/lib/jsonapi/active_record_record_accessor.rb +++ b/lib/jsonapi/active_record_record_accessor.rb @@ -10,6 +10,14 @@ def transaction end end + def rollback_transaction + fail ActiveRecord::Rollback + end + + def model_error_messages(model) + model.errors.messages + end + def model_base_class ActiveRecord::Base end @@ -22,16 +30,8 @@ def record_not_found_error_class ActiveRecord::RecordNotFound end - def rollback_transaction - fail ActiveRecord::Rollback - end - - def model_error_messages(model) - model.errors.messages - end - def association_model_class_name(from_model, relationship_name) - from_model.reflect_on_association(relationship_name) + (reflect = from_model.reflect_on_association(relationship_name)) && reflect.class_name end def find_resource(filters, options = {}) diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index c32fd2816..0b2a1d487 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -224,7 +224,7 @@ def _remove fail JSONAPI::Exceptions::ValidationErrors.new(self) end :completed - self.class._record_accessor.model_error_messages(_model) + rescue self.class._record_accessor.delete_restriction_error_class => e fail JSONAPI::Exceptions::RecordLocked.new(e.message) end @@ -915,15 +915,15 @@ def _add_relationship(klass, *attrs) # ResourceBuilder methods def define_relationship_methods(relationship_name, relationship_klass, options) # Initialize from an ORM model's properties - if _model_class.is_a?(_record_accessor.model_base_class) && - (association_model_class_name = association_model_class_name(_model_class, relationship_name)) + if _model_class && _model_class < _record_accessor.model_base_class && + (association_model_class_name = _record_accessor.association_model_class_name(_model_class, relationship_name)) options = options.reverse_merge(class_name: association_model_class_name) end relationship = register_relationship( - relationship_name, - relationship_klass.new(relationship_name, options) + relationship_name, + relationship_klass.new(relationship_name, options) ) define_foreign_key_setter(relationship) diff --git a/lib/jsonapi/sequel_record_accessor.rb b/lib/jsonapi/sequel_record_accessor.rb index 0465907ea..bc60085a6 100644 --- a/lib/jsonapi/sequel_record_accessor.rb +++ b/lib/jsonapi/sequel_record_accessor.rb @@ -18,7 +18,7 @@ def model_error_messages(model) end def model_base_class - Sequel + Sequel::Model end def delete_restriction_error_class @@ -29,6 +29,11 @@ def record_not_found_error_class ActiveRecord::RecordNotFound end + def association_model_class_name(from_model, relationship_name) + (reflect = from_model.association_reflections[relationship_name]) && + reflect[:class_name] && reflect[:class_name].gsub(/^::/, '') # Sequel puts "::" in the beginning + end + def find_resource(filters, options = {}) if options[:caching] && options[:caching][:cache_serializer_output] find_serialized_with_caching(filters, options[:caching][:serializer], options) diff --git a/test/fixtures/sequel.rb b/test/fixtures/sequel.rb deleted file mode 100644 index 62326f732..000000000 --- a/test/fixtures/sequel.rb +++ /dev/null @@ -1,1877 +0,0 @@ -require 'active_record' -require 'jsonapi-resources' - -ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.uncountable 'preferences' - inflect.irregular 'numero_telefone', 'numeros_telefone' -end - -### DATABASE -ActiveRecord::Schema.define do - create_table :people, force: true do |t| - t.string :name - t.string :email - t.datetime :date_joined - t.belongs_to :preferences - t.integer :hair_cut_id, index: true - t.boolean :book_admin, default: false - t.boolean :special, default: false - t.timestamps null: false - end - - create_table :author_details, force: true do |t| - t.integer :person_id - t.string :author_stuff - end - - create_table :posts, force: true do |t| - t.string :title, length: 255 - t.text :body - t.integer :author_id - t.integer :parent_post_id - t.belongs_to :section, index: true - t.timestamps null: false - end - - create_table :comments, force: true do |t| - t.text :body - t.belongs_to :post, index: true - t.integer :author_id - t.timestamps null: false - end - - create_table :companies, force: true do |t| - t.string :type - t.string :name - t.string :address - t.timestamps null: false - end - - create_table :tags, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :sections, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :posts_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :posts_tags, [:post_id, :tag_id], unique: true - - create_table :special_post_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :special_post_tags, [:post_id, :tag_id], unique: true - - create_table :comments_tags, force: true do |t| - t.references :comment, :tag, index: true - end - - create_table :iso_currencies, id: false, force: true do |t| - t.string :code, limit: 3, null: false - t.string :name - t.string :country_name - t.string :minor_unit - t.timestamps null: false - end - add_index :iso_currencies, :code, unique: true - - create_table :expense_entries, force: true do |t| - t.string :currency_code, limit: 3, null: false - t.integer :employee_id, null: false - t.decimal :cost, precision: 12, scale: 4, null: false - t.date :transaction_date - t.timestamps null: false - end - - create_table :planets, force: true do |t| - t.string :name - t.string :description - t.integer :planet_type_id - end - - create_table :planets_tags, force: true do |t| - t.references :planet, :tag, index: true - end - add_index :planets_tags, [:planet_id, :tag_id], unique: true - - create_table :planet_types, force: true do |t| - t.string :name - end - - create_table :moons, force: true do |t| - t.string :name - t.string :description - t.integer :planet_id - t.timestamps null: false - end - - create_table :craters, id: false, force: true do |t| - t.string :code - t.string :description - t.integer :moon_id - t.timestamps null: false - end - - create_table :preferences, force: true do |t| - t.integer :person_id - t.boolean :advanced_mode, default: false - t.timestamps null: false - end - - create_table :facts, force: true do |t| - t.integer :person_id - t.string :spouse_name - t.text :bio - t.float :quality_rating - t.decimal :salary, precision: 12, scale: 2 - t.datetime :date_time_joined - t.date :birthday - t.time :bedtime - t.binary :photo, limit: 1.kilobyte - t.boolean :cool - t.timestamps null: false - end - - create_table :books, force: true do |t| - t.string :title - t.string :isbn - t.boolean :banned, default: false - t.timestamps null: false - end - - create_table :book_authors, force: true do |t| - t.integer :book_id - t.integer :person_id - end - - create_table :book_comments, force: true do |t| - t.text :body - t.belongs_to :book, index: true - t.integer :author_id - t.boolean :approved, default: true - t.timestamps null: false - end - - create_table :customers, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :purchase_orders, force: true do |t| - t.date :order_date - t.date :requested_delivery_date - t.date :delivery_date - t.integer :customer_id - t.string :delivery_name - t.string :delivery_address_1 - t.string :delivery_address_2 - t.string :delivery_city - t.string :delivery_state - t.string :delivery_postal_code - t.float :delivery_fee - t.float :tax - t.float :total - t.timestamps null: false - end - - create_table :order_flags, force: true do |t| - t.string :name - end - - create_table :purchase_orders_order_flags, force: true do |t| - t.references :purchase_order, :order_flag, index: true - end - add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" - - create_table :line_items, force: true do |t| - t.integer :purchase_order_id - t.string :part_number - t.string :quantity - t.float :item_cost - t.timestamps null: false - end - - create_table :hair_cuts, force: true do |t| - t.string :style - end - - create_table :numeros_telefone, force: true do |t| - t.string :numero_telefone - t.timestamps null: false - end - - create_table :categories, force: true do |t| - t.string :name - t.string :status, limit: 10 - t.timestamps null: false - end - - create_table :pictures, force: true do |t| - t.string :name - t.integer :imageable_id - t.string :imageable_type - t.timestamps null: false - end - - create_table :documents, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :products, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :vehicles, force: true do |t| - t.string :type - t.string :make - t.string :model - t.string :length_at_water_line - t.string :drive_layout - t.string :serial_number - t.integer :person_id - t.timestamps null: false - end - - create_table :makes, force: true do |t| - t.string :model - t.timestamps null: false - end - - # special cases - fields that look like they should be reserved names - create_table :hrefs, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :links, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :web_pages, force: true do |t| - t.string :href - t.string :link - t.timestamps null: false - end - - create_table :questionables, force: true do |t| - t.timestamps null: false - end - - create_table :boxes, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :things, force: true do |t| - t.string :name - t.references :user - t.references :box - - t.timestamps null: false - end - - create_table :users, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :related_things, force: true do |t| - t.string :name - t.references :from, references: :thing - t.references :to, references: :thing - - t.timestamps null: false - end - - create_table :questions, force: true do |t| - t.string :text - end - - create_table :answers, force: true do |t| - t.references :question - t.integer :respondent_id - t.string :respondent_type - t.string :text - end - - create_table :patients, force: true do |t| - t.string :name - end - - create_table :doctors, force: true do |t| - t.string :name - end - - # special cases -end - -### MODELS -class Person < ActiveRecord::Base - has_many :posts, foreign_key: 'author_id' - has_many :comments, foreign_key: 'author_id' - has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception - has_many :vehicles - belongs_to :preferences - belongs_to :hair_cut - has_one :author_detail - - has_and_belongs_to_many :books, join_table: :book_authors - - has_many :even_posts, -> { where('posts.id % 2 = 0') }, class_name: 'Post', foreign_key: 'author_id' - has_many :odd_posts, -> { where('posts.id % 2 = 1') }, class_name: 'Post', foreign_key: 'author_id' - - ### Validations - validates :name, presence: true - validates :date_joined, presence: true -end - -class AuthorDetail < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'person_id' -end - -class Post < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' - has_many :comments - has_and_belongs_to_many :tags, join_table: :posts_tags - has_many :special_post_tags, source: :tag - has_many :special_tags, through: :special_post_tags, source: :tag - belongs_to :section - has_one :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' - - validates :author, presence: true - validates :title, length: { maximum: 35 } - - before_destroy :destroy_callback - - def destroy_callback - if title == "can't destroy me" - errors.add(:title, "can't destroy me") - - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end - end -end - -class SpecialPostTag < ActiveRecord::Base - belongs_to :tag - belongs_to :post -end - -class Comment < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :post - has_and_belongs_to_many :tags, join_table: :comments_tags -end - -class Company < ActiveRecord::Base -end - -class Firm < Company -end - -class Tag < ActiveRecord::Base - has_and_belongs_to_many :posts, join_table: :posts_tags - has_and_belongs_to_many :planets, join_table: :planets_tags -end - -class Section < ActiveRecord::Base - has_many :posts -end - -class HairCut < ActiveRecord::Base - has_many :people -end - -class Property < ActiveRecord::Base -end - -class Customer < ActiveRecord::Base -end - -class BadlyNamedAttributes < ActiveRecord::Base -end - -class Cat < ActiveRecord::Base -end - -class IsoCurrency < ActiveRecord::Base - self.primary_key = :code - # has_many :expense_entries, foreign_key: 'currency_code' -end - -class ExpenseEntry < ActiveRecord::Base - belongs_to :employee, class_name: 'Person', foreign_key: 'employee_id' - belongs_to :iso_currency, foreign_key: 'currency_code' -end - -class Planet < ActiveRecord::Base - has_many :moons - belongs_to :planet_type - - has_and_belongs_to_many :tags, join_table: :planets_tags - - # Test model callback cancelling save - before_save :check_not_pluto - - def check_not_pluto - # Pluto can't be a planet, so cancel the save - if name.downcase == 'pluto' - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end - end -end - -class PlanetType < ActiveRecord::Base - has_many :planets -end - -class Moon < ActiveRecord::Base - belongs_to :planet - - has_many :craters -end - -class Crater < ActiveRecord::Base - self.primary_key = :code - - belongs_to :moon -end - -class Preferences < ActiveRecord::Base - has_one :author, class_name: 'Person', :inverse_of => 'preferences' -end - -class Fact < ActiveRecord::Base - validates :spouse_name, :bio, presence: true -end - -class Like < ActiveRecord::Base -end - -class Breed - - def initialize(id = nil, name = nil) - if id.nil? - @id = $breed_data.new_id - $breed_data.add(self) - else - @id = id - end - @name = name - @errors = ActiveModel::Errors.new(self) - end - - attr_accessor :id, :name - - def destroy - $breed_data.remove(@id) - end - - def valid?(context = nil) - @errors.clear - if name.is_a?(String) && name.length > 0 - return true - else - @errors.add(:name, "can't be blank") - return false - end - end - - def errors - @errors - end -end - -class Book < ActiveRecord::Base - has_many :book_comments - has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" - - has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" -end - -class BookComment < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :book - - def self.for_user(current_user) - records = self - # Hide the unapproved comments from people who are not book admins - unless current_user && current_user.book_admin - records = records.where(approved: true) - end - records - end -end - -class BreedData - def initialize - @breeds = {} - end - - def breeds - @breeds - end - - def new_id - @breeds.keys.max + 1 - end - - def add(breed) - @breeds[breed.id] = breed - end - - def remove(id) - @breeds.delete(id) - end -end - -class Customer < ActiveRecord::Base - has_many :purchase_orders -end - -class PurchaseOrder < ActiveRecord::Base - belongs_to :customer - has_many :line_items - has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id' - - has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags - - has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag' -end - -class OrderFlag < ActiveRecord::Base - has_and_belongs_to_many :purchase_orders, join_table: :purchase_orders_order_flags -end - -class LineItem < ActiveRecord::Base - belongs_to :purchase_order -end - -class NumeroTelefone < ActiveRecord::Base -end - -class Category < ActiveRecord::Base -end - -class Picture < ActiveRecord::Base - belongs_to :imageable, polymorphic: true -end - -class Vehicle < ActiveRecord::Base - belongs_to :person -end - -class Car < Vehicle -end - -class Boat < Vehicle -end - -class Document < ActiveRecord::Base - has_many :pictures, as: :imageable -end - -class Document::Topic < Document -end - -class Product < ActiveRecord::Base - has_one :picture, as: :imageable -end - -class Make < ActiveRecord::Base -end - -class WebPage < ActiveRecord::Base -end - -class Box < ActiveRecord::Base - has_many :things -end - -class User < ActiveRecord::Base - has_many :things -end - -class Thing < ActiveRecord::Base - belongs_to :box - belongs_to :user - - has_many :related_things, foreign_key: :from_id - has_many :things, through: :related_things, source: :to -end - -class RelatedThing < ActiveRecord::Base - belongs_to :from, class_name: Thing, foreign_key: :from_id - belongs_to :to, class_name: Thing, foreign_key: :to_id -end - -class Question < ActiveRecord::Base - has_one :answer - - def respondent - answer.try(:respondent) - end -end - -class Answer < ActiveRecord::Base - belongs_to :question - belongs_to :respondent, polymorphic: true -end - -class Patient < ActiveRecord::Base -end - -class Doctor < ActiveRecord::Base -end - -module Api - module V7 - class Client < Customer - end - - class Customer < Customer - end - end -end - -### CONTROLLERS -class AuthorsController < JSONAPI::ResourceControllerMetal -end - -class PeopleController < JSONAPI::ResourceController -end - -class BaseController < ActionController::Base - include JSONAPI::ActsAsResourceController -end - -class PostsController < BaseController - - class SpecialError < StandardError; end - class SubSpecialError < PostsController::SpecialError; end - class SerializeError < StandardError; end - - # This is used to test that classes that are whitelisted are reraised by - # the operations dispatcher. - rescue_from PostsController::SpecialError do - head :forbidden - end - - #called by test_on_server_error - def self.set_callback_message(error) - @callback_message = "Sent from method" - end - - def resource_serializer_klass - PostSerializer - end -end - -class PostSerializer < JSONAPI::ResourceSerializer - def initialize(*) - if $PostSerializerRaisesErrors - raise PostsController::SerializeError - else - super - end - end -end - -class CommentsController < JSONAPI::ResourceController -end - -class FirmsController < JSONAPI::ResourceController -end - -class SectionsController < JSONAPI::ResourceController -end - -class TagsController < JSONAPI::ResourceController -end - -class IsoCurrenciesController < JSONAPI::ResourceController -end - -class ExpenseEntriesController < JSONAPI::ResourceController -end - -class BreedsController < JSONAPI::ResourceController -end - -class FactsController < JSONAPI::ResourceController -end - -class CategoriesController < JSONAPI::ResourceController -end - -class PicturesController < JSONAPI::ResourceController -end - -class DocumentsController < JSONAPI::ResourceController -end - -class ProductsController < JSONAPI::ResourceController -end - -class ImageablesController < JSONAPI::ResourceController -end - -class VehiclesController < JSONAPI::ResourceController -end - -class CarsController < JSONAPI::ResourceController -end - -class BoatsController < JSONAPI::ResourceController -end - -class BooksController < JSONAPI::ResourceController - def context - { title: 'Title' } - end -end - -### CONTROLLERS -module Api - module V1 - class AuthorsController < JSONAPI::ResourceController - end - - class PeopleController < JSONAPI::ResourceController - end - - class PostsController < ActionController::Base - include JSONAPI::ActsAsResourceController - end - - class TagsController < JSONAPI::ResourceController - end - - class IsoCurrenciesController < JSONAPI::ResourceController - end - - class ExpenseEntriesController < JSONAPI::ResourceController - end - - class BreedsController < JSONAPI::ResourceController - end - - class PlanetsController < JSONAPI::ResourceController - end - - class PlanetTypesController < JSONAPI::ResourceController - end - - class MoonsController < JSONAPI::ResourceController - end - - class CratersController < JSONAPI::ResourceController - def context - {current_user: $test_user} - end - end - - class LikesController < JSONAPI::ResourceController - end - end - - module V2 - class AuthorsController < JSONAPI::ResourceController - end - - class PeopleController < JSONAPI::ResourceController - end - - class PostsController < JSONAPI::ResourceController - end - - class PreferencesController < JSONAPI::ResourceController - end - - class BooksController < JSONAPI::ResourceController - def context - {current_user: $test_user} - end - end - - class BookCommentsController < JSONAPI::ResourceController - def context - {current_user: $test_user} - end - end - end - - module V3 - class PostsController < JSONAPI::ResourceController - end - end - - module V4 - class PostsController < JSONAPI::ResourceController - end - - class ExpenseEntriesController < JSONAPI::ResourceController - end - - class IsoCurrenciesController < JSONAPI::ResourceController - end - - class BooksController < JSONAPI::ResourceController - end - end - - module V5 - class AuthorsController < JSONAPI::ResourceController - def serialization_options - {foo: 'bar'} - end - end - - class PostsController < JSONAPI::ResourceController - end - - class ExpenseEntriesController < JSONAPI::ResourceController - end - - class IsoCurrenciesController < JSONAPI::ResourceController - end - end - - module V6 - class PostsController < JSONAPI::ResourceController - end - - class SectionsController < JSONAPI::ResourceController - end - - class CustomersController < JSONAPI::ResourceController - end - - class PurchaseOrdersController < JSONAPI::ResourceController - def context - {current_user: $test_user} - end - end - - class LineItemsController < JSONAPI::ResourceController - end - - class OrderFlagsController < JSONAPI::ResourceController - end - end - - module V7 - class CustomersController < JSONAPI::ResourceController - end - - class PurchaseOrdersController < JSONAPI::ResourceController - end - - class LineItemsController < JSONAPI::ResourceController - end - - class OrderFlagsController < JSONAPI::ResourceController - end - - class CategoriesController < JSONAPI::ResourceController - end - - class ClientsController < JSONAPI::ResourceController - end - end - - module V8 - class NumerosTelefoneController < JSONAPI::ResourceController - end - end -end - -module Api - class BoxesController < JSONAPI::ResourceController - end -end - -class QuestionsController < JSONAPI::ResourceController -end - -class AnswersController < JSONAPI::ResourceController -end - -class PatientsController < JSONAPI::ResourceController -end - -class DoctorsController < JSONAPI::ResourceController -end - -class RespondentController < JSONAPI::ResourceController -end - -### RESOURCES -class BaseResource < JSONAPI::Resource - abstract -end - -class PersonResource < BaseResource - attributes :name, :email - attribute :date_joined, format: :date_with_timezone - - has_many :comments, :posts - has_many :vehicles, polymorphic: true - - has_one :preferences - has_one :hair_cut - - filter :name, verify: :verify_name_filter - - def self.verify_name_filter(values, _context) - values.each do |value| - if value.length < 3 - raise JSONAPI::Exceptions::InvalidFilterValue.new(:name, value) - end - end - return values - end - -end - -class PersonWithEvenAndOddPostsResource < JSONAPI::Resource - model_name 'Person' - - has_many :even_posts, foreign_key: 'author_id', class_name: 'Post', relation_name: :even_posts - has_many :odd_posts, foreign_key: 'author_id', class_name: 'Post', relation_name: :odd_posts -end - -class SpecialBaseResource < BaseResource - abstract - - model_hint model: Person, resource: :special_person -end - -class SpecialPersonResource < SpecialBaseResource - model_name 'Person' - - def self.records(options = {}) - Person.where(special: true) - end -end - -class VehicleResource < JSONAPI::Resource - immutable - - has_one :person - attributes :make, :model, :serial_number -end - -class CarResource < VehicleResource - attributes :drive_layout -end - -class BoatResource < VehicleResource - attributes :length_at_water_line -end - -class CommentResource < JSONAPI::Resource - attributes :body - has_one :post - has_one :author, class_name: 'Person' - has_many :tags - - filters :body -end - -class CompanyResource < JSONAPI::Resource - attributes :name, :address -end - -class FirmResource < CompanyResource - model_name "Firm" -end - -class TagResource < JSONAPI::Resource - attributes :name - - has_many :posts - # Not including the planets relationship so they don't get output - #has_many :planets -end - -class SectionResource < JSONAPI::Resource - attributes 'name' -end - -module ParentApi - class PostResource < JSONAPI::Resource - model_name 'Post' - attributes :title - has_one :parent_post - end -end - -class PostResource < JSONAPI::Resource - attribute :title - attribute :body - attribute :subject - - has_one :author, class_name: 'Person' - has_one :section - has_many :tags, acts_as_set: true, inverse_relationship: :posts, eager_load_on_include: false - has_many :comments, acts_as_set: false, inverse_relationship: :post - - # Not needed - just for testing - primary_key :id - - def self.default_sort - [{field: 'title', direction: :desc}, {field: 'id', direction: :desc}] - end - - before_save do - msg = "Before save" - end - - after_save do - msg = "After save" - end - - before_update do - msg = "Before update" - end - - after_update do - msg = "After update" - end - - before_replace_fields do - msg = "Before replace_fields" - end - - after_replace_fields do - msg = "After replace_fields" - end - - around_update :around_update_check - - def around_update_check - # do nothing - yield - # do nothing - end - - def subject - @model.title - end - - def title=(title) - @model.title = title - if title == 'BOOM' - raise 'The Server just tested going boom. If this was a real emergency you would be really dead right now.' - end - end - - filters :title, :author, :tags, :comments - filter :id, verify: ->(values, context) { - verify_keys(values, context) - return values - } - filter :ids, - verify: ->(values, context) { - verify_keys(values, context) - return values - }, - apply: -> (records, value, _options) { - records.where('id IN (?)', value) - } - - filter :search, - verify: ->(values, context) { - values.all?{|v| (v.is_a?(Hash) || v.is_a?(ActionController::Parameters)) } && values - }, - apply: -> (records, values, _options) { - records.where(title: values.first['title']) - } - - def self.updatable_fields(context) - super(context) - [:author, :subject] - end - - def self.creatable_fields(context) - super(context) - [:subject] - end - - def self.sortable_fields(context) - super(context) - [:id] + [:"author.name"] - end - - def self.verify_key(key, context = nil) - super(key) - raise JSONAPI::Exceptions::RecordNotFound.new(key) unless find_by_key(key, context: context) - return key - end -end - -class HairCutResource < JSONAPI::Resource - attribute :style - has_many :people -end - -class IsoCurrencyResource < JSONAPI::Resource - attributes :name, :country_name, :minor_unit - attribute :id, format: :id, readonly: false - - filter :country_name - - key_type :string -end - -class ExpenseEntryResource < JSONAPI::Resource - attributes :cost - attribute :transaction_date, format: :date - - has_one :iso_currency, foreign_key: 'currency_code' - has_one :employee, class_name: 'Person' -end - -class EmployeeResource < JSONAPI::Resource - attributes :name, :email - model_name 'Person' -end - -class BreedResource < JSONAPI::Resource - attribute :name, format: :title - - # This is unneeded, just here for testing - routing_options param: :id - - def self.find(filters, options = {}) - breeds = [] - $breed_data.breeds.values.each do |breed| - breeds.push(BreedResource.new(breed, options[:context])) - end - breeds - end - - def self.find_by_key(id, options = {}) - BreedResource.new($breed_data.breeds[id.to_i], options[:context]) - end - - def _save - super - return :accepted - end -end - -class PlanetResource < JSONAPI::Resource - attribute :name - attribute :description - - has_many :moons - has_one :planet_type - - has_many :tags, acts_as_set: true -end - -class PropertyResource < JSONAPI::Resource - attributes :name - - has_many :planets -end - -class PlanetTypeResource < JSONAPI::Resource - attributes :name - has_many :planets, inverse_relationship: :planet_type -end - -class MoonResource < JSONAPI::Resource - attribute :name - attribute :description - - has_one :planet - has_many :craters -end - -class CraterResource < JSONAPI::Resource - attribute :code - attribute :description - - has_one :moon - - filter :description, apply: -> (records, value, options) { - fail "context not set" unless options[:context][:current_user] != nil && options[:context][:current_user] == $test_user - records.where(:description => value) - } - - def self.verify_key(key, context = nil) - key && String(key) - end -end - -class PreferencesResource < JSONAPI::Resource - attribute :advanced_mode - - has_one :author, :foreign_key_on => :related - - def self.find_records(filters, options = {}) - Preferences.limit(1) - end -end - -class FactResource < JSONAPI::Resource - attribute :spouse_name - attribute :bio - attribute :quality_rating - attribute :salary - attribute :date_time_joined - attribute :birthday - attribute :bedtime - attribute :photo - attribute :cool -end - -class CategoryResource < JSONAPI::Resource - filter :status, default: 'active' -end - -class PictureResource < JSONAPI::Resource - attribute :name - has_one :imageable, polymorphic: true -end - -class DocumentResource < JSONAPI::Resource - attribute :name - has_many :pictures -end - -class TopicResource < JSONAPI::Resource - model_name 'Document::Topic' - has_many :pictures -end - -class ProductResource < JSONAPI::Resource - attribute :name - has_one :picture, always_include_linkage_data: true - - def picture_id - _model.picture.id - end -end - -class ImageableResource < JSONAPI::Resource -end - -class MakeResource < JSONAPI::Resource - attribute :model -end - -class WebPageResource < JSONAPI::Resource - attribute :href - attribute :link -end - -class AuthorResource < JSONAPI::Resource - model_name 'Person' - attributes :name - - has_many :books, inverse_relationship: :authors -end - -class BookResource < JSONAPI::Resource - attribute :title - - has_many :authors, class_name: 'Author', inverse_relationship: :books - - def title - context[:title] - end -end - -class AuthorDetailResource < JSONAPI::Resource - attributes :author_stuff -end - -class SimpleCustomLinkResource < JSONAPI::Resource - model_name 'Post' - attributes :title, :body, :subject - - def subject - @model.title - end - - has_one :writer, foreign_key: 'author_id', class_name: 'Writer' - has_one :section - has_many :comments, acts_as_set: false - - filters :writer - - def custom_links(options) - { raw: options[:serializer].link_builder.self_link(self) + "/raw" } - end -end - -class CustomLinkWithRelativePathOptionResource < JSONAPI::Resource - model_name 'Post' - attributes :title, :body, :subject - - def subject - @model.title - end - - has_one :writer, foreign_key: 'author_id', class_name: 'Writer' - has_one :section - has_many :comments, acts_as_set: false - - filters :writer - - def custom_links(options) - { raw: options[:serializer].link_builder.self_link(self) + "/super/duper/path.xml" } - end -end - -class CustomLinkWithIfCondition < JSONAPI::Resource - model_name 'Post' - attributes :title, :body, :subject - - def subject - @model.title - end - - has_one :writer, foreign_key: 'author_id', class_name: 'Writer' - has_one :section - has_many :comments, acts_as_set: false - - filters :writer - - def custom_links(options) - if title == "JR Solves your serialization woes!" - {conditional_custom_link: options[:serializer].link_builder.self_link(self) + "/conditional/link.json"} - end - end -end - -class CustomLinkWithLambda < JSONAPI::Resource - model_name 'Post' - attributes :title, :body, :subject, :created_at - - def subject - @model.title - end - - has_one :writer, foreign_key: 'author_id', class_name: 'Writer' - has_one :section - has_many :comments, acts_as_set: false - - filters :writer - - def custom_links(options) - { - link_to_external_api: "http://external-api.com/posts/#{ created_at.year }/#{ created_at.month }/#{ created_at.day }-#{ subject.gsub(' ', '-') }" - } - end -end - -module Api - module V1 - class WriterResource < JSONAPI::Resource - attributes :name, :email - model_name 'Person' - has_many :posts - - filter :name - end - - class LikeResource < JSONAPI::Resource - end - - class PostResource < JSONAPI::Resource - # V1 no longer supports tags and now calls author 'writer' - attribute :title - attribute :body - attribute :subject - - has_one :writer, foreign_key: 'author_id', class_name: 'Writer' - has_one :section - has_many :comments, acts_as_set: false - - def self.default_sort - [{field: 'title', direction: :asc}, {field: 'id', direction: :desc}] - end - - def subject - @model.title - end - - filters :writer - end - - class PersonResource < PersonResource; end - class CommentResource < CommentResource; end - class TagResource < TagResource; end - class SectionResource < SectionResource; end - class IsoCurrencyResource < IsoCurrencyResource; end - class ExpenseEntryResource < ExpenseEntryResource; end - class BreedResource < BreedResource; end - class PlanetResource < PlanetResource; end - class PlanetTypeResource < PlanetTypeResource; end - class MoonResource < MoonResource; end - class CraterResource < CraterResource; end - class PreferencesResource < PreferencesResource; end - class EmployeeResource < EmployeeResource; end - class HairCutResource < HairCutResource; end - class VehicleResource < VehicleResource; end - class CarResource < CarResource; end - class BoatResource < BoatResource; end - end -end - -module Api - module V2 - class PreferencesResource < PreferencesResource; end - class PersonResource < PersonResource; end - class PostResource < PostResource; end - - class BookResource < JSONAPI::Resource - attribute :title - attributes :isbn, :banned - - has_many :authors - - has_many :book_comments, relation_name: -> (options = {}) { - context = options[:context] - current_user = context ? context[:current_user] : nil - - unless current_user && current_user.book_admin - :approved_book_comments - else - :book_comments - end - }, reflect: true - - has_many :aliased_comments, class_name: 'BookComments', relation_name: :approved_book_comments - - filters :book_comments - filter :banned, apply: :apply_filter_banned - - class << self - def books - Book.arel_table - end - - def not_banned_books - books[:banned].eq(false) - end - - def records(options = {}) - context = options[:context] - current_user = context ? context[:current_user] : nil - - records = _model_class - # Hide the banned books from people who are not book admins - unless current_user && current_user.book_admin - records = records.where(not_banned_books) - end - records - end - - def apply_filter_banned(records, value, options) - context = options[:context] - current_user = context ? context[:current_user] : nil - - # Only book admins might filter for banned books - if current_user && current_user.book_admin - records.where('books.banned = ?', value[0] == 'true') - end - end - - end - end - - class BookCommentResource < JSONAPI::Resource - attributes :body, :approved - - has_one :book - has_one :author, class_name: 'Person' - - filters :book - filter :approved, apply: ->(records, value, options) { - context = options[:context] - current_user = context ? context[:current_user] : nil - - if current_user && current_user.book_admin - records.where(approved_comments(value[0] == 'true')) - end - } - - class << self - def book_comments - BookComment.arel_table - end - - def approved_comments(approved = true) - book_comments[:approved].eq(approved) - end - - def records(options = {}) - current_user = options[:context][:current_user] - _model_class.for_user(current_user) - end - end - end - end -end - -module Api - module V3 - class PostResource < PostResource; end - class PreferencesResource < PreferencesResource; end - end -end - -module Api - module V4 - class PostResource < PostResource; end - class PersonResource < PersonResource; end - class ExpenseEntryResource < ExpenseEntryResource; end - class IsoCurrencyResource < IsoCurrencyResource; end - - class BookResource < Api::V2::BookResource - paginator :paged - end - - class BookCommentResource < Api::V2::BookCommentResource - paginator :paged - end - end -end - -module Api - module V5 - class AuthorResource < JSONAPI::Resource - attributes :name, :email - model_name 'Person' - relationship :posts, to: :many - relationship :author_detail, to: :one, foreign_key_on: :related - - filter :name - - def self.find_records(filters, options = {}) - rel = _model_class - filters.each do |attr, filter| - if attr.to_s == "id" - rel = rel.where(id: filter) - else - rel = rel.where("\"#{attr}\" LIKE \"%#{filter[0]}%\"") - end - end - rel - end - - def fetchable_fields - super - [:email] - end - end - - class AuthorDetailResource < JSONAPI::Resource - attributes :author_stuff - end - - class PersonResource < PersonResource; end - class PostResource < PostResource; end - class TagResource < TagResource; end - class SectionResource < SectionResource; end - class CommentResource < CommentResource; end - class ExpenseEntryResource < ExpenseEntryResource; end - class IsoCurrencyResource < IsoCurrencyResource; end - class EmployeeResource < EmployeeResource; end - end -end - -module Api - module V6 - class PersonResource < PersonResource; end - class TagResource < TagResource; end - - class SectionResource < SectionResource - has_many :posts - end - - class CommentResource < CommentResource; end - - class PostResource < PostResource - # Test caching with SQL fragments - def self.records(options = {}) - _model_class.all.joins('INNER JOIN people on people.id = author_id') - end - end - - class CustomerResource < JSONAPI::Resource - attribute :name - - has_many :purchase_orders - end - - class PurchaseOrderResource < JSONAPI::Resource - attribute :order_date - attribute :requested_delivery_date - attribute :delivery_date - attribute :delivery_name - attribute :delivery_address_1 - attribute :delivery_address_2 - attribute :delivery_city - attribute :delivery_state - attribute :delivery_postal_code - attribute :delivery_fee - attribute :tax - attribute :total - - has_one :customer - has_many :line_items, relation_name: -> (options = {}) { - context = options[:context] - current_user = context ? context[:current_user] : nil - - unless current_user && current_user.book_admin - :line_items - else - :admin_line_items - end - }, - reflect: false - - has_many :order_flags, acts_as_set: true, - relation_name: -> (options = {}) { - context = options[:context] - current_user = context ? context[:current_user] : nil - - unless current_user && current_user.book_admin - :order_flags - else - :admin_order_flags - end - } - end - - class OrderFlagResource < JSONAPI::Resource - attributes :name - - has_many :purchase_orders, reflect: false - end - - class LineItemResource < JSONAPI::Resource - attribute :part_number - attribute :quantity - attribute :item_cost - - has_one :purchase_order - end - end - - module V7 - class PurchaseOrderResource < V6::PurchaseOrderResource; end - class OrderFlagResource < V6::OrderFlagResource; end - class LineItemResource < V6::LineItemResource; end - - class CustomerResource < V6::CustomerResource - model_name 'Api::V7::Customer' - end - - class ClientResource < JSONAPI::Resource - model_name 'Api::V7::Customer' - - attribute :name - - has_many :purchase_orders - end - - class CategoryResource < CategoryResource - attribute :name - - # Raise exception for failure in controller - def name - fail "Something Exceptional Happened" - end - end - end - - module V8 - class NumeroTelefoneResource < JSONAPI::Resource - attribute :numero_telefone - end - end -end - -module AdminApi - module V1 - class PersonResource < JSONAPI::Resource - end - end -end - -module DasherizedNamespace - module V1 - class PersonResource < JSONAPI::Resource - end - end -end - -module MyEngine - module Api - module V1 - class PersonResource < JSONAPI::Resource - end - end - end - - module AdminApi - module V1 - class PersonResource < JSONAPI::Resource - end - end - end - - module DasherizedNamespace - module V1 - class PersonResource < JSONAPI::Resource - end - end - end -end - -module ApiV2Engine - class PersonResource < JSONAPI::Resource - end -end - -module Legacy - class FlatPost < ActiveRecord::Base - self.table_name = "posts" - end -end - -class FlatPostResource < JSONAPI::Resource - model_name "Legacy::FlatPost", add_model_hint: false - - model_hint model: "Legacy::FlatPost", resource: FlatPostResource - - attribute :title -end - -class FlatPostsController < JSONAPI::ResourceController -end - -# CustomProcessors -class Api::V4::BookProcessor < JSONAPI::Processor - after_find do - unless @results.is_a?(JSONAPI::ErrorsOperationResult) - @result.meta[:total_records] = @result.record_count - @result.links['spec'] = 'https://test_corp.com' - end - end -end - -class PostProcessor < JSONAPI::Processor - def find - if $PostProcessorRaisesErrors - raise PostsController::SubSpecialError - end - # puts("In custom Operations Processor without Namespace") - super - end - - after_find do - unless @results.is_a?(JSONAPI::ErrorsOperationResult) - @result.meta[:total_records] = @result.record_count - @result.links['spec'] = 'https://test_corp.com' - end - end -end - -module Api - module V7 - class CategoryProcessor < JSONAPI::Processor - def show - if $PostProcessorRaisesErrors - raise PostsController::SubSpecialError - end - # puts("In custom Operations Processor without Namespace") - super - end - end - end -end - -module Api - module V1 - class PostProcessor < JSONAPI::Processor - def show - # puts("In custom Operations Processor with Namespace") - super - end - end - end -end - -module Api - class BoxResource < JSONAPI::Resource - has_many :things - end - - class ThingResource < JSONAPI::Resource - has_one :box - has_one :user - - has_many :things - end - - class UserResource < JSONAPI::Resource - has_many :things - end -end - -class QuestionResource < JSONAPI::Resource - has_one :answer - has_one :respondent, polymorphic: true, class_name: "Respondent", foreign_key_on: :related - - attributes :text -end - -class AnswerResource < JSONAPI::Resource - has_one :question - has_one :respondent, polymorphic: true -end - -class PatientResource < JSONAPI::Resource - attributes :name -end - -class DoctorResource < JSONAPI::Resource - attributes :name -end - -class RespondentResource < JSONAPI::Resource - abstract -end - -### PORO Data - don't do this in a production app -$breed_data = BreedData.new -$breed_data.add(Breed.new(0, 'persian')) -$breed_data.add(Breed.new(1, 'siamese')) -$breed_data.add(Breed.new(2, 'sphinx')) -$breed_data.add(Breed.new(3, 'to_delete')) diff --git a/test/fixtures/active_record.rb b/test/support/controllers_resources_processors.rb similarity index 65% rename from test/fixtures/active_record.rb rename to test/support/controllers_resources_processors.rb index 62326f732..f3305f304 100644 --- a/test/fixtures/active_record.rb +++ b/test/support/controllers_resources_processors.rb @@ -1,659 +1,4 @@ -require 'active_record' -require 'jsonapi-resources' - -ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.uncountable 'preferences' - inflect.irregular 'numero_telefone', 'numeros_telefone' -end - -### DATABASE -ActiveRecord::Schema.define do - create_table :people, force: true do |t| - t.string :name - t.string :email - t.datetime :date_joined - t.belongs_to :preferences - t.integer :hair_cut_id, index: true - t.boolean :book_admin, default: false - t.boolean :special, default: false - t.timestamps null: false - end - - create_table :author_details, force: true do |t| - t.integer :person_id - t.string :author_stuff - end - - create_table :posts, force: true do |t| - t.string :title, length: 255 - t.text :body - t.integer :author_id - t.integer :parent_post_id - t.belongs_to :section, index: true - t.timestamps null: false - end - - create_table :comments, force: true do |t| - t.text :body - t.belongs_to :post, index: true - t.integer :author_id - t.timestamps null: false - end - - create_table :companies, force: true do |t| - t.string :type - t.string :name - t.string :address - t.timestamps null: false - end - - create_table :tags, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :sections, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :posts_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :posts_tags, [:post_id, :tag_id], unique: true - - create_table :special_post_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :special_post_tags, [:post_id, :tag_id], unique: true - - create_table :comments_tags, force: true do |t| - t.references :comment, :tag, index: true - end - - create_table :iso_currencies, id: false, force: true do |t| - t.string :code, limit: 3, null: false - t.string :name - t.string :country_name - t.string :minor_unit - t.timestamps null: false - end - add_index :iso_currencies, :code, unique: true - - create_table :expense_entries, force: true do |t| - t.string :currency_code, limit: 3, null: false - t.integer :employee_id, null: false - t.decimal :cost, precision: 12, scale: 4, null: false - t.date :transaction_date - t.timestamps null: false - end - - create_table :planets, force: true do |t| - t.string :name - t.string :description - t.integer :planet_type_id - end - - create_table :planets_tags, force: true do |t| - t.references :planet, :tag, index: true - end - add_index :planets_tags, [:planet_id, :tag_id], unique: true - - create_table :planet_types, force: true do |t| - t.string :name - end - - create_table :moons, force: true do |t| - t.string :name - t.string :description - t.integer :planet_id - t.timestamps null: false - end - - create_table :craters, id: false, force: true do |t| - t.string :code - t.string :description - t.integer :moon_id - t.timestamps null: false - end - - create_table :preferences, force: true do |t| - t.integer :person_id - t.boolean :advanced_mode, default: false - t.timestamps null: false - end - - create_table :facts, force: true do |t| - t.integer :person_id - t.string :spouse_name - t.text :bio - t.float :quality_rating - t.decimal :salary, precision: 12, scale: 2 - t.datetime :date_time_joined - t.date :birthday - t.time :bedtime - t.binary :photo, limit: 1.kilobyte - t.boolean :cool - t.timestamps null: false - end - - create_table :books, force: true do |t| - t.string :title - t.string :isbn - t.boolean :banned, default: false - t.timestamps null: false - end - - create_table :book_authors, force: true do |t| - t.integer :book_id - t.integer :person_id - end - - create_table :book_comments, force: true do |t| - t.text :body - t.belongs_to :book, index: true - t.integer :author_id - t.boolean :approved, default: true - t.timestamps null: false - end - - create_table :customers, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :purchase_orders, force: true do |t| - t.date :order_date - t.date :requested_delivery_date - t.date :delivery_date - t.integer :customer_id - t.string :delivery_name - t.string :delivery_address_1 - t.string :delivery_address_2 - t.string :delivery_city - t.string :delivery_state - t.string :delivery_postal_code - t.float :delivery_fee - t.float :tax - t.float :total - t.timestamps null: false - end - - create_table :order_flags, force: true do |t| - t.string :name - end - - create_table :purchase_orders_order_flags, force: true do |t| - t.references :purchase_order, :order_flag, index: true - end - add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" - - create_table :line_items, force: true do |t| - t.integer :purchase_order_id - t.string :part_number - t.string :quantity - t.float :item_cost - t.timestamps null: false - end - - create_table :hair_cuts, force: true do |t| - t.string :style - end - - create_table :numeros_telefone, force: true do |t| - t.string :numero_telefone - t.timestamps null: false - end - - create_table :categories, force: true do |t| - t.string :name - t.string :status, limit: 10 - t.timestamps null: false - end - - create_table :pictures, force: true do |t| - t.string :name - t.integer :imageable_id - t.string :imageable_type - t.timestamps null: false - end - - create_table :documents, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :products, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :vehicles, force: true do |t| - t.string :type - t.string :make - t.string :model - t.string :length_at_water_line - t.string :drive_layout - t.string :serial_number - t.integer :person_id - t.timestamps null: false - end - - create_table :makes, force: true do |t| - t.string :model - t.timestamps null: false - end - - # special cases - fields that look like they should be reserved names - create_table :hrefs, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :links, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :web_pages, force: true do |t| - t.string :href - t.string :link - t.timestamps null: false - end - - create_table :questionables, force: true do |t| - t.timestamps null: false - end - - create_table :boxes, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :things, force: true do |t| - t.string :name - t.references :user - t.references :box - - t.timestamps null: false - end - - create_table :users, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :related_things, force: true do |t| - t.string :name - t.references :from, references: :thing - t.references :to, references: :thing - - t.timestamps null: false - end - - create_table :questions, force: true do |t| - t.string :text - end - - create_table :answers, force: true do |t| - t.references :question - t.integer :respondent_id - t.string :respondent_type - t.string :text - end - - create_table :patients, force: true do |t| - t.string :name - end - - create_table :doctors, force: true do |t| - t.string :name - end - - # special cases -end - -### MODELS -class Person < ActiveRecord::Base - has_many :posts, foreign_key: 'author_id' - has_many :comments, foreign_key: 'author_id' - has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception - has_many :vehicles - belongs_to :preferences - belongs_to :hair_cut - has_one :author_detail - - has_and_belongs_to_many :books, join_table: :book_authors - - has_many :even_posts, -> { where('posts.id % 2 = 0') }, class_name: 'Post', foreign_key: 'author_id' - has_many :odd_posts, -> { where('posts.id % 2 = 1') }, class_name: 'Post', foreign_key: 'author_id' - - ### Validations - validates :name, presence: true - validates :date_joined, presence: true -end - -class AuthorDetail < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'person_id' -end - -class Post < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' - has_many :comments - has_and_belongs_to_many :tags, join_table: :posts_tags - has_many :special_post_tags, source: :tag - has_many :special_tags, through: :special_post_tags, source: :tag - belongs_to :section - has_one :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' - - validates :author, presence: true - validates :title, length: { maximum: 35 } - - before_destroy :destroy_callback - - def destroy_callback - if title == "can't destroy me" - errors.add(:title, "can't destroy me") - - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end - end -end - -class SpecialPostTag < ActiveRecord::Base - belongs_to :tag - belongs_to :post -end - -class Comment < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :post - has_and_belongs_to_many :tags, join_table: :comments_tags -end - -class Company < ActiveRecord::Base -end - -class Firm < Company -end - -class Tag < ActiveRecord::Base - has_and_belongs_to_many :posts, join_table: :posts_tags - has_and_belongs_to_many :planets, join_table: :planets_tags -end - -class Section < ActiveRecord::Base - has_many :posts -end - -class HairCut < ActiveRecord::Base - has_many :people -end - -class Property < ActiveRecord::Base -end - -class Customer < ActiveRecord::Base -end - -class BadlyNamedAttributes < ActiveRecord::Base -end - -class Cat < ActiveRecord::Base -end - -class IsoCurrency < ActiveRecord::Base - self.primary_key = :code - # has_many :expense_entries, foreign_key: 'currency_code' -end - -class ExpenseEntry < ActiveRecord::Base - belongs_to :employee, class_name: 'Person', foreign_key: 'employee_id' - belongs_to :iso_currency, foreign_key: 'currency_code' -end - -class Planet < ActiveRecord::Base - has_many :moons - belongs_to :planet_type - - has_and_belongs_to_many :tags, join_table: :planets_tags - - # Test model callback cancelling save - before_save :check_not_pluto - - def check_not_pluto - # Pluto can't be a planet, so cancel the save - if name.downcase == 'pluto' - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end - end -end - -class PlanetType < ActiveRecord::Base - has_many :planets -end - -class Moon < ActiveRecord::Base - belongs_to :planet - - has_many :craters -end - -class Crater < ActiveRecord::Base - self.primary_key = :code - - belongs_to :moon -end - -class Preferences < ActiveRecord::Base - has_one :author, class_name: 'Person', :inverse_of => 'preferences' -end - -class Fact < ActiveRecord::Base - validates :spouse_name, :bio, presence: true -end - -class Like < ActiveRecord::Base -end - -class Breed - - def initialize(id = nil, name = nil) - if id.nil? - @id = $breed_data.new_id - $breed_data.add(self) - else - @id = id - end - @name = name - @errors = ActiveModel::Errors.new(self) - end - - attr_accessor :id, :name - - def destroy - $breed_data.remove(@id) - end - - def valid?(context = nil) - @errors.clear - if name.is_a?(String) && name.length > 0 - return true - else - @errors.add(:name, "can't be blank") - return false - end - end - - def errors - @errors - end -end - -class Book < ActiveRecord::Base - has_many :book_comments - has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" - - has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" -end - -class BookComment < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :book - - def self.for_user(current_user) - records = self - # Hide the unapproved comments from people who are not book admins - unless current_user && current_user.book_admin - records = records.where(approved: true) - end - records - end -end - -class BreedData - def initialize - @breeds = {} - end - - def breeds - @breeds - end - - def new_id - @breeds.keys.max + 1 - end - - def add(breed) - @breeds[breed.id] = breed - end - - def remove(id) - @breeds.delete(id) - end -end - -class Customer < ActiveRecord::Base - has_many :purchase_orders -end - -class PurchaseOrder < ActiveRecord::Base - belongs_to :customer - has_many :line_items - has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id' - - has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags - - has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag' -end - -class OrderFlag < ActiveRecord::Base - has_and_belongs_to_many :purchase_orders, join_table: :purchase_orders_order_flags -end - -class LineItem < ActiveRecord::Base - belongs_to :purchase_order -end - -class NumeroTelefone < ActiveRecord::Base -end - -class Category < ActiveRecord::Base -end - -class Picture < ActiveRecord::Base - belongs_to :imageable, polymorphic: true -end - -class Vehicle < ActiveRecord::Base - belongs_to :person -end - -class Car < Vehicle -end - -class Boat < Vehicle -end - -class Document < ActiveRecord::Base - has_many :pictures, as: :imageable -end - -class Document::Topic < Document -end - -class Product < ActiveRecord::Base - has_one :picture, as: :imageable -end - -class Make < ActiveRecord::Base -end - -class WebPage < ActiveRecord::Base -end - -class Box < ActiveRecord::Base - has_many :things -end - -class User < ActiveRecord::Base - has_many :things -end - -class Thing < ActiveRecord::Base - belongs_to :box - belongs_to :user - - has_many :related_things, foreign_key: :from_id - has_many :things, through: :related_things, source: :to -end - -class RelatedThing < ActiveRecord::Base - belongs_to :from, class_name: Thing, foreign_key: :from_id - belongs_to :to, class_name: Thing, foreign_key: :to_id -end - -class Question < ActiveRecord::Base - has_one :answer - - def respondent - answer.try(:respondent) - end -end - -class Answer < ActiveRecord::Base - belongs_to :question - belongs_to :respondent, polymorphic: true -end - -class Patient < ActiveRecord::Base -end - -class Doctor < ActiveRecord::Base -end - -module Api - module V7 - class Client < Customer - end - - class Customer < Customer - end - end -end +# Controllers, Resources, and Processors for specs. ### CONTROLLERS class AuthorsController < JSONAPI::ResourceControllerMetal diff --git a/test/support/models.rb b/test/support/models.rb new file mode 100644 index 000000000..f323a7662 --- /dev/null +++ b/test/support/models.rb @@ -0,0 +1,350 @@ +require 'active_record' + +# Here are the models specifically used for fixtures. + + +### MODELS +module FixtureModel + + def self.class_mapping + [ BookComment, Person, AuthorDetail, Post, SpecialPostTag, Comment, Company, Firm, Tag, Section, HairCut, Property, + Customer, BadlyNamedAttributes, Cat, IsoCurrency, ExpenseEntry, Planet, PlanetType, Moon, Crater, Preferences, + Fact, Like, Breed, Book, BookComment, BreedData, Customer, PurchaseOrder, OrderFlag, LineItem, + NumeroTelefone, Category, Picture, Vehicle, Car, Boat, Document, Document, Product, Make, WebPage, + Box, User, Thing, RelatedThing, Question, Answer, Patient, Doctor].inject({}) do |hash, klass| + hash[klass.to_s.demodulize.tableize] = klass + hash + end + end + + class Person < ActiveRecord::Base + has_many :posts, foreign_key: 'author_id' + has_many :comments, foreign_key: 'author_id' + has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception + has_many :vehicles + belongs_to :preferences + belongs_to :hair_cut + has_one :author_detail + + has_and_belongs_to_many :books, join_table: :book_authors + + has_many :even_posts, -> { where('posts.id % 2 = 0') }, class_name: 'Post', foreign_key: 'author_id' + has_many :odd_posts, -> { where('posts.id % 2 = 1') }, class_name: 'Post', foreign_key: 'author_id' + + ### Validations + validates :name, presence: true + validates :date_joined, presence: true + end + + class AuthorDetail < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'person_id' + end + + class Post < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' + has_many :comments + has_and_belongs_to_many :tags, join_table: :posts_tags + has_many :special_post_tags, source: :tag + has_many :special_tags, through: :special_post_tags, source: :tag + belongs_to :section + has_one :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' + + validates :author, presence: true + validates :title, length: { maximum: 35 } + + before_destroy :destroy_callback + + def destroy_callback + if title == "can't destroy me" + errors.add(:title, "can't destroy me") + + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end + end + + class SpecialPostTag < ActiveRecord::Base + belongs_to :tag + belongs_to :post + end + + class Comment < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :post + has_and_belongs_to_many :tags, join_table: :comments_tags + end + + class Company < ActiveRecord::Base + end + + class Firm < Company + end + + class Tag < ActiveRecord::Base + has_and_belongs_to_many :posts, join_table: :posts_tags + has_and_belongs_to_many :planets, join_table: :planets_tags + end + + class Section < ActiveRecord::Base + has_many :posts + end + + class HairCut < ActiveRecord::Base + has_many :people + end + + class Property < ActiveRecord::Base + end + + class Customer < ActiveRecord::Base + end + + class BadlyNamedAttributes < ActiveRecord::Base + end + + class Cat < ActiveRecord::Base + end + + class IsoCurrency < ActiveRecord::Base + self.primary_key = :code + # has_many :expense_entries, foreign_key: 'currency_code' + end + + class ExpenseEntry < ActiveRecord::Base + belongs_to :employee, class_name: 'Person', foreign_key: 'employee_id' + belongs_to :iso_currency, foreign_key: 'currency_code' + end + + class Planet < ActiveRecord::Base + has_many :moons + belongs_to :planet_type + + has_and_belongs_to_many :tags, join_table: :planets_tags + + # Test model callback cancelling save + before_save :check_not_pluto + + def check_not_pluto + # Pluto can't be a planet, so cancel the save + if name.downcase == 'pluto' + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end + end + + class PlanetType < ActiveRecord::Base + has_many :planets + end + + class Moon < ActiveRecord::Base + belongs_to :planet + + has_many :craters + end + + class Crater < ActiveRecord::Base + self.primary_key = :code + + belongs_to :moon + end + + class Preferences < ActiveRecord::Base + has_one :author, class_name: 'Person', :inverse_of => 'preferences' + end + + class Fact < ActiveRecord::Base + validates :spouse_name, :bio, presence: true + end + + class Like < ActiveRecord::Base + end + + class Breed + + def initialize(id = nil, name = nil) + if id.nil? + @id = $breed_data.new_id + $breed_data.add(self) + else + @id = id + end + @name = name + @errors = ActiveModel::Errors.new(self) + end + + attr_accessor :id, :name + + def destroy + $breed_data.remove(@id) + end + + def valid?(context = nil) + @errors.clear + if name.is_a?(String) && name.length > 0 + return true + else + @errors.add(:name, "can't be blank") + return false + end + end + + def errors + @errors + end + end + + class Book < ActiveRecord::Base + has_many :book_comments + has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" + + has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" + end + + class BookComment < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :book + + def self.for_user(current_user) + records = self + # Hide the unapproved comments from people who are not book admins + unless current_user && current_user.book_admin + records = records.where(approved: true) + end + records + end + end + + class BreedData + def initialize + @breeds = {} + end + + def breeds + @breeds + end + + def new_id + @breeds.keys.max + 1 + end + + def add(breed) + @breeds[breed.id] = breed + end + + def remove(id) + @breeds.delete(id) + end + end + + class Customer < ActiveRecord::Base + has_many :purchase_orders + end + + class PurchaseOrder < ActiveRecord::Base + belongs_to :customer + has_many :line_items + has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id' + + has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags + + has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag' + end + + class OrderFlag < ActiveRecord::Base + has_and_belongs_to_many :purchase_orders, join_table: :purchase_orders_order_flags + end + + class LineItem < ActiveRecord::Base + belongs_to :purchase_order + end + + class NumeroTelefone < ActiveRecord::Base + end + + class Category < ActiveRecord::Base + end + + class Picture < ActiveRecord::Base + belongs_to :imageable, polymorphic: true + end + + class Vehicle < ActiveRecord::Base + belongs_to :person + end + + class Car < Vehicle + end + + class Boat < Vehicle + end + + class Document < ActiveRecord::Base + has_many :pictures, as: :imageable + end + + class Document::Topic < Document + end + + class Product < ActiveRecord::Base + has_one :picture, as: :imageable + end + + class Make < ActiveRecord::Base + end + + class WebPage < ActiveRecord::Base + end + + class Box < ActiveRecord::Base + has_many :things + end + + class User < ActiveRecord::Base + has_many :things + end + + class Thing < ActiveRecord::Base + belongs_to :box + belongs_to :user + + has_many :related_things, foreign_key: :from_id + has_many :things, through: :related_things, source: :to + end + + class RelatedThing < ActiveRecord::Base + belongs_to :from, class_name: Thing, foreign_key: :from_id + belongs_to :to, class_name: Thing, foreign_key: :to_id + end + + class Question < ActiveRecord::Base + has_one :answer + + def respondent + answer.try(:respondent) + end + end + + class Answer < ActiveRecord::Base + belongs_to :question + belongs_to :respondent, polymorphic: true + end + + class Patient < ActiveRecord::Base + end + + class Doctor < ActiveRecord::Base + end +end \ No newline at end of file diff --git a/test/support/orm/active_record/initialize.rb b/test/support/orm/active_record/initialize.rb new file mode 100644 index 000000000..1f7ec46c0 --- /dev/null +++ b/test/support/orm/active_record/initialize.rb @@ -0,0 +1 @@ +require 'active_record/railtie' \ No newline at end of file diff --git a/test/support/orm/active_record/models.rb b/test/support/orm/active_record/models.rb new file mode 100644 index 000000000..021ecae83 --- /dev/null +++ b/test/support/orm/active_record/models.rb @@ -0,0 +1,354 @@ +require_relative 'schema' + +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.uncountable 'preferences' + inflect.irregular 'numero_telefone', 'numeros_telefone' +end + +class Person < ActiveRecord::Base + has_many :posts, foreign_key: 'author_id' + has_many :comments, foreign_key: 'author_id' + has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception + has_many :vehicles + belongs_to :preferences + belongs_to :hair_cut + has_one :author_detail + + has_and_belongs_to_many :books, join_table: :book_authors + + has_many :even_posts, -> { where('posts.id % 2 = 0') }, class_name: 'Post', foreign_key: 'author_id' + has_many :odd_posts, -> { where('posts.id % 2 = 1') }, class_name: 'Post', foreign_key: 'author_id' + + ### Validations + validates :name, presence: true + validates :date_joined, presence: true +end + +class AuthorDetail < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'person_id' +end + +class Post < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' + has_many :comments + has_and_belongs_to_many :tags, join_table: :posts_tags + has_many :special_post_tags, source: :tag + has_many :special_tags, through: :special_post_tags, source: :tag + belongs_to :section + has_one :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' + + validates :author, presence: true + validates :title, length: { maximum: 35 } + + before_destroy :destroy_callback + + def destroy_callback + if title == "can't destroy me" + errors.add(:title, "can't destroy me") + + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class SpecialPostTag < ActiveRecord::Base + belongs_to :tag + belongs_to :post +end + +class Comment < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :post + has_and_belongs_to_many :tags, join_table: :comments_tags +end + +class Company < ActiveRecord::Base +end + +class Firm < Company +end + +class Tag < ActiveRecord::Base + has_and_belongs_to_many :posts, join_table: :posts_tags + has_and_belongs_to_many :planets, join_table: :planets_tags +end + +class Section < ActiveRecord::Base + has_many :posts +end + +class HairCut < ActiveRecord::Base + has_many :people +end + +class Property < ActiveRecord::Base +end + +class Customer < ActiveRecord::Base +end + +class BadlyNamedAttributes < ActiveRecord::Base +end + +class Cat < ActiveRecord::Base +end + +class IsoCurrency < ActiveRecord::Base + self.primary_key = :code + # has_many :expense_entries, foreign_key: 'currency_code' +end + +class ExpenseEntry < ActiveRecord::Base + belongs_to :employee, class_name: 'Person', foreign_key: 'employee_id' + belongs_to :iso_currency, foreign_key: 'currency_code' +end + +class Planet < ActiveRecord::Base + has_many :moons + belongs_to :planet_type + + has_and_belongs_to_many :tags, join_table: :planets_tags + + # Test model callback cancelling save + before_save :check_not_pluto + + def check_not_pluto + # Pluto can't be a planet, so cancel the save + if name.downcase == 'pluto' + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class PlanetType < ActiveRecord::Base + has_many :planets +end + +class Moon < ActiveRecord::Base + belongs_to :planet + + has_many :craters +end + +class Crater < ActiveRecord::Base + self.primary_key = :code + + belongs_to :moon +end + +class Preferences < ActiveRecord::Base + has_one :author, class_name: 'Person', :inverse_of => 'preferences' +end + +class Fact < ActiveRecord::Base + validates :spouse_name, :bio, presence: true +end + +class Like < ActiveRecord::Base +end + +class Breed + + def initialize(id = nil, name = nil) + if id.nil? + @id = $breed_data.new_id + $breed_data.add(self) + else + @id = id + end + @name = name + @errors = ActiveModel::Errors.new(self) + end + + attr_accessor :id, :name + + def destroy + $breed_data.remove(@id) + end + + def valid?(context = nil) + @errors.clear + if name.is_a?(String) && name.length > 0 + return true + else + @errors.add(:name, "can't be blank") + return false + end + end + + def errors + @errors + end +end + +class Book < ActiveRecord::Base + has_many :book_comments + has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" + + has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" +end + +class BookComment < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :book + + def self.for_user(current_user) + records = self + # Hide the unapproved comments from people who are not book admins + unless current_user && current_user.book_admin + records = records.where(approved: true) + end + records + end +end + +class BreedData + def initialize + @breeds = {} + end + + def breeds + @breeds + end + + def new_id + @breeds.keys.max + 1 + end + + def add(breed) + @breeds[breed.id] = breed + end + + def remove(id) + @breeds.delete(id) + end +end + +class Customer < ActiveRecord::Base + has_many :purchase_orders +end + +class PurchaseOrder < ActiveRecord::Base + belongs_to :customer + has_many :line_items + has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id' + + has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags + + has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag' +end + +class OrderFlag < ActiveRecord::Base + has_and_belongs_to_many :purchase_orders, join_table: :purchase_orders_order_flags +end + +class LineItem < ActiveRecord::Base + belongs_to :purchase_order +end + +class NumeroTelefone < ActiveRecord::Base +end + +class Category < ActiveRecord::Base +end + +class Picture < ActiveRecord::Base + belongs_to :imageable, polymorphic: true +end + +class Vehicle < ActiveRecord::Base + belongs_to :person +end + +class Car < Vehicle +end + +class Boat < Vehicle +end + +class Document < ActiveRecord::Base + has_many :pictures, as: :imageable +end + +class Document::Topic < Document +end + +class Product < ActiveRecord::Base + has_one :picture, as: :imageable +end + +class Make < ActiveRecord::Base +end + +class WebPage < ActiveRecord::Base +end + +class Box < ActiveRecord::Base + has_many :things +end + +class User < ActiveRecord::Base + has_many :things +end + +class Thing < ActiveRecord::Base + belongs_to :box + belongs_to :user + + has_many :related_things, foreign_key: :from_id + has_many :things, through: :related_things, source: :to +end + +class RelatedThing < ActiveRecord::Base + belongs_to :from, class_name: Thing, foreign_key: :from_id + belongs_to :to, class_name: Thing, foreign_key: :to_id +end + +class Question < ActiveRecord::Base + has_one :answer + + def respondent + answer.try(:respondent) + end +end + +class Answer < ActiveRecord::Base + belongs_to :question + belongs_to :respondent, polymorphic: true +end + +class Patient < ActiveRecord::Base +end + +class Doctor < ActiveRecord::Base +end + +module Api + module V7 + class Client < Customer + end + + class Customer < Customer + end + end +end + +### PORO Data - don't do this in a production app +$breed_data = BreedData.new +$breed_data.add(Breed.new(0, 'persian')) +$breed_data.add(Breed.new(1, 'siamese')) +$breed_data.add(Breed.new(2, 'sphinx')) +$breed_data.add(Breed.new(3, 'to_delete')) diff --git a/test/support/orm/active_record/schema.rb b/test/support/orm/active_record/schema.rb new file mode 100644 index 000000000..2b76e9b11 --- /dev/null +++ b/test/support/orm/active_record/schema.rb @@ -0,0 +1,310 @@ +require 'active_record' + +ActiveRecord::Schema.verbose = false + +### DATABASE +ActiveRecord::Schema.define do + create_table :people, force: true do |t| + t.string :name + t.string :email + t.datetime :date_joined + t.belongs_to :preferences + t.integer :hair_cut_id, index: true + t.boolean :book_admin, default: false + t.boolean :special, default: false + t.timestamps null: false + end + + create_table :author_details, force: true do |t| + t.integer :person_id + t.string :author_stuff + end + + create_table :posts, force: true do |t| + t.string :title, length: 255 + t.text :body + t.integer :author_id + t.integer :parent_post_id + t.belongs_to :section, index: true + t.timestamps null: false + end + + create_table :comments, force: true do |t| + t.text :body + t.belongs_to :post, index: true + t.integer :author_id + t.timestamps null: false + end + + create_table :companies, force: true do |t| + t.string :type + t.string :name + t.string :address + t.timestamps null: false + end + + create_table :tags, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :sections, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :posts_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :posts_tags, [:post_id, :tag_id], unique: true + + create_table :special_post_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :special_post_tags, [:post_id, :tag_id], unique: true + + create_table :comments_tags, force: true do |t| + t.references :comment, :tag, index: true + end + + create_table :iso_currencies, id: false, force: true do |t| + t.string :code, limit: 3, null: false + t.string :name + t.string :country_name + t.string :minor_unit + t.timestamps null: false + end + add_index :iso_currencies, :code, unique: true + + create_table :expense_entries, force: true do |t| + t.string :currency_code, limit: 3, null: false + t.integer :employee_id, null: false + t.decimal :cost, precision: 12, scale: 4, null: false + t.date :transaction_date + t.timestamps null: false + end + + create_table :planets, force: true do |t| + t.string :name + t.string :description + t.integer :planet_type_id + end + + create_table :planets_tags, force: true do |t| + t.references :planet, :tag, index: true + end + add_index :planets_tags, [:planet_id, :tag_id], unique: true + + create_table :planet_types, force: true do |t| + t.string :name + end + + create_table :moons, force: true do |t| + t.string :name + t.string :description + t.integer :planet_id + t.timestamps null: false + end + + create_table :craters, id: false, force: true do |t| + t.string :code + t.string :description + t.integer :moon_id + t.timestamps null: false + end + + create_table :preferences, force: true do |t| + t.integer :person_id + t.boolean :advanced_mode, default: false + t.timestamps null: false + end + + create_table :facts, force: true do |t| + t.integer :person_id + t.string :spouse_name + t.text :bio + t.float :quality_rating + t.decimal :salary, precision: 12, scale: 2 + t.datetime :date_time_joined + t.date :birthday + t.time :bedtime + t.binary :photo, limit: 1.kilobyte + t.boolean :cool + t.timestamps null: false + end + + create_table :books, force: true do |t| + t.string :title + t.string :isbn + t.boolean :banned, default: false + t.timestamps null: false + end + + create_table :book_authors, force: true do |t| + t.integer :book_id + t.integer :person_id + end + + create_table :book_comments, force: true do |t| + t.text :body + t.belongs_to :book, index: true + t.integer :author_id + t.boolean :approved, default: true + t.timestamps null: false + end + + create_table :customers, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :purchase_orders, force: true do |t| + t.date :order_date + t.date :requested_delivery_date + t.date :delivery_date + t.integer :customer_id + t.string :delivery_name + t.string :delivery_address_1 + t.string :delivery_address_2 + t.string :delivery_city + t.string :delivery_state + t.string :delivery_postal_code + t.float :delivery_fee + t.float :tax + t.float :total + t.timestamps null: false + end + + create_table :order_flags, force: true do |t| + t.string :name + end + + create_table :purchase_orders_order_flags, force: true do |t| + t.references :purchase_order, :order_flag, index: true + end + add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" + + create_table :line_items, force: true do |t| + t.integer :purchase_order_id + t.string :part_number + t.string :quantity + t.float :item_cost + t.timestamps null: false + end + + create_table :hair_cuts, force: true do |t| + t.string :style + end + + create_table :numeros_telefone, force: true do |t| + t.string :numero_telefone + t.timestamps null: false + end + + create_table :categories, force: true do |t| + t.string :name + t.string :status, limit: 10 + t.timestamps null: false + end + + create_table :pictures, force: true do |t| + t.string :name + t.integer :imageable_id + t.string :imageable_type + t.timestamps null: false + end + + create_table :documents, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :products, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :vehicles, force: true do |t| + t.string :type + t.string :make + t.string :model + t.string :length_at_water_line + t.string :drive_layout + t.string :serial_number + t.integer :person_id + t.timestamps null: false + end + + create_table :makes, force: true do |t| + t.string :model + t.timestamps null: false + end + + # special cases - fields that look like they should be reserved names + create_table :hrefs, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :links, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :web_pages, force: true do |t| + t.string :href + t.string :link + t.timestamps null: false + end + + create_table :questionables, force: true do |t| + t.timestamps null: false + end + + create_table :boxes, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :things, force: true do |t| + t.string :name + t.references :user + t.references :box + + t.timestamps null: false + end + + create_table :users, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :related_things, force: true do |t| + t.string :name + t.references :from, references: :thing + t.references :to, references: :thing + + t.timestamps null: false + end + + create_table :questions, force: true do |t| + t.string :text + end + + create_table :answers, force: true do |t| + t.references :question + t.integer :respondent_id + t.string :respondent_type + t.string :text + end + + create_table :patients, force: true do |t| + t.string :name + end + + create_table :doctors, force: true do |t| + t.string :name + end + + # special cases +end \ No newline at end of file diff --git a/test/support/orm/active_record/setup.rb b/test/support/orm/active_record/setup.rb new file mode 100644 index 000000000..20aae25f5 --- /dev/null +++ b/test/support/orm/active_record/setup.rb @@ -0,0 +1,29 @@ + +JSONAPI.configuration.default_record_accessor_klass = JSONAPI::ActiveRecordRecordAccessor + +TestApp.class_eval do + config.active_record.schema_format = :none + + if Rails::VERSION::MAJOR >= 5 + config.active_support.halt_callback_chains_on_return_false = false + config.active_record.time_zone_aware_types = [:time, :datetime] + config.active_record.belongs_to_required_by_default = false + end +end + +class Minitest::Test + include ActiveRecord::TestFixtures + + self.fixture_path = "#{Rails.root}/fixtures" + fixtures :all +end + +class ActiveSupport::TestCase + self.fixture_path = "#{Rails.root}/fixtures" + fixtures :all +end + +class ActionDispatch::IntegrationTest + self.fixture_path = "#{Rails.root}/fixtures" + fixtures :all +end \ No newline at end of file diff --git a/test/support/orm/sequel/initialize.rb b/test/support/orm/sequel/initialize.rb new file mode 100644 index 000000000..48354f18b --- /dev/null +++ b/test/support/orm/sequel/initialize.rb @@ -0,0 +1,2 @@ +require 'sequel_rails' +require 'sequel_rails/sequel/database/active_support_notification' \ No newline at end of file diff --git a/test/support/orm/sequel/models.rb b/test/support/orm/sequel/models.rb new file mode 100644 index 000000000..2c132f740 --- /dev/null +++ b/test/support/orm/sequel/models.rb @@ -0,0 +1,371 @@ +require 'sequel' +require 'jsonapi-resources' +require_relative 'schema' + +config = Rails.configuration.database_configuration["test"] +config["adapter"] = "sqlite" if config["adapter"]=="sqlite3" +Sequel.connect(config) + +Sequel::Model.class_eval do + plugin :validation_class_methods + plugin :hook_class_methods + plugin :timestamps, update_on_create: true + plugin :single_table_inheritance, :type +end + +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.uncountable 'preferences' + inflect.irregular 'numero_telefone', 'numeros_telefone' +end + +### MODELS +class Person < Sequel::Model + one_to_many :posts, key: 'author_id' + one_to_many :comments, key: 'author_id' + one_to_many :expense_entries, key: 'employee_id', dependent: :restrict_with_exception + one_to_many :vehicles + many_to_one :preferences + many_to_one :hair_cut + one_to_one :author_detail + + many_to_many :books, join_table: :book_authors + + one_to_many :even_posts, conditions: 'posts.id % 2 = 0', class: 'Post', key: 'author_id' + one_to_many :odd_posts, conditions: 'posts.id % 2 = 1', class: 'Post', key: 'author_id' + + ### Validations + validates_presence_of :name, :date_joined +end + +class AuthorDetail < Sequel::Model + many_to_one :author, class: 'Person', key: 'person_id' +end + +class Post < Sequel::Model + many_to_one :author, class: 'Person', key: 'author_id' + many_to_one :writer, class: 'Person', key: 'author_id' + one_to_many :comments + many_to_many :tags, join_table: :posts_tags + one_to_many :special_post_tags, source: :tag + one_to_many :special_tags, through: :special_post_tags, source: :tag + many_to_one :section + one_to_one :parent_post, class: 'Post', key: 'parent_post_id' + + validates_presence_of :author + validates_length_of :title, maximum: 35 + + before_destroy :destroy_callback + + def destroy_callback + if title == "can't destroy me" + errors.add(:title, "can't destroy me") + + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class SpecialPostTag < Sequel::Model + many_to_one :tag + many_to_one :post +end + +class Comment < Sequel::Model + many_to_one :author, class: 'Person', key: 'author_id' + many_to_one :post + many_to_many :tags, join_table: :comments_tags +end + +class Company < Sequel::Model +end + +class Firm < Company +end + +class Tag < Sequel::Model + many_to_many :posts, join_table: :posts_tags + many_to_many :planets, join_table: :planets_tags +end + +class Section < Sequel::Model + one_to_many :posts +end + +class HairCut < Sequel::Model + one_to_many :people +end + +class Property < Sequel::Model +end + +class Customer < Sequel::Model +end + +class BadlyNamedAttributes < Sequel::Model +end + +class Cat < Sequel::Model +end + +class IsoCurrency < Sequel::Model + set_primary_key :code + # one_to_many :expense_entries, key: 'currency_code' +end + +class ExpenseEntry < Sequel::Model + many_to_one :employee, class: 'Person', key: 'employee_id' + many_to_one :iso_currency, key: 'currency_code' +end + +class Planet < Sequel::Model + one_to_many :moons + many_to_one :planet_type + + many_to_many :tags, join_table: :planets_tags + + # Test model callback cancelling save + before_save :check_not_pluto + + def check_not_pluto + # Pluto can't be a planet, so cancel the save + if name.downcase == 'pluto' + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + end + end +end + +class PlanetType < Sequel::Model + one_to_many :planets +end + +class Moon < Sequel::Model + many_to_one :planet + + one_to_many :craters +end + +class Crater < Sequel::Model + set_primary_key :code + + many_to_one :moon +end + +class Preferences < Sequel::Model + one_to_one :author, class: 'Person', :inverse_of => 'preferences' +end + +class Fact < Sequel::Model + validates_presence_of :spouse_name, :bio +end + +class Like < Sequel::Model +end + +class Breed + + def initialize(id = nil, name = nil) + if id.nil? + @id = $breed_data.new_id + $breed_data.add(self) + else + @id = id + end + @name = name + @errors = Sequel::Model::Errors.new + end + + attr_accessor :id, :name + + def destroy + $breed_data.remove(@id) + end + + def valid?(context = nil) + @errors.clear + if name.is_a?(String) && name.length > 0 + return true + else + @errors.add(:name, "can't be blank") + return false + end + end + + def errors + @errors + end +end + +class Book < Sequel::Model + one_to_many :book_comments + one_to_many :approved_book_comments, conditions: {approved: true}, class: "BookComment" + + many_to_many :authors, join_table: :book_authors, class: "Person" +end + +class BookComment < Sequel::Model + many_to_one :author, class: 'Person', key: 'author_id' + many_to_one :book + + def before_save + debugger + end + + def self.for_user(current_user) + records = self + # Hide the unapproved comments from people who are not book admins + unless current_user && current_user.book_admin + records = records.where(approved: true) + end + records + end +end + +class BreedData + def initialize + @breeds = {} + end + + def breeds + @breeds + end + + def new_id + @breeds.keys.max + 1 + end + + def add(breed) + @breeds[breed.id] = breed + end + + def remove(id) + @breeds.delete(id) + end +end + +class Customer < Sequel::Model + one_to_many :purchase_orders +end + +class PurchaseOrder < Sequel::Model + many_to_one :customer + one_to_many :line_items + one_to_many :admin_line_items, class: 'LineItem', key: 'purchase_order_id' + + many_to_many :order_flags, join_table: :purchase_orders_order_flags + + many_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class: 'OrderFlag' +end + +class OrderFlag < Sequel::Model + many_to_many :purchase_orders, join_table: :purchase_orders_order_flags +end + +class LineItem < Sequel::Model + many_to_one :purchase_order +end + +class NumeroTelefone < Sequel::Model +end + +class Category < Sequel::Model +end + +class Picture < Sequel::Model + many_to_one :imageable, polymorphic: true +end + +class Vehicle < Sequel::Model + many_to_one :person +end + +class Car < Vehicle +end + +class Boat < Vehicle +end + +class Document < Sequel::Model + one_to_many :pictures, as: :imageable +end + +class Document::Topic < Document +end + +class Product < Sequel::Model + one_to_one :picture, as: :imageable +end + +class Make < Sequel::Model +end + +class WebPage < Sequel::Model +end + +class Box < Sequel::Model + one_to_many :things +end + +class User < Sequel::Model + one_to_many :things +end + +class Thing < Sequel::Model + many_to_one :box + many_to_one :user + + one_to_many :related_things, key: :from_id + one_to_many :things, through: :related_things, source: :to +end + +class RelatedThing < Sequel::Model + many_to_one :from, class: Thing, key: :from_id + many_to_one :to, class: Thing, key: :to_id +end + +class Question < Sequel::Model + one_to_one :answer + + def respondent + answer.try(:respondent) + end +end + +class Answer < Sequel::Model + many_to_one :question + many_to_one :respondent, polymorphic: true +end + +class Patient < Sequel::Model +end + +class Doctor < Sequel::Model +end + +module Api + module V7 + class Client < Customer + end + + class Customer < Customer + end + end +end + +### PORO Data - don't do this in a production app +$breed_data = BreedData.new +$breed_data.add(Breed.new(0, 'persian')) +$breed_data.add(Breed.new(1, 'siamese')) +$breed_data.add(Breed.new(2, 'sphinx')) +$breed_data.add(Breed.new(3, 'to_delete')) diff --git a/test/support/orm/sequel/setup.rb b/test/support/orm/sequel/setup.rb new file mode 100644 index 000000000..1ace5e634 --- /dev/null +++ b/test/support/orm/sequel/setup.rb @@ -0,0 +1,12 @@ + +JSONAPI.configuration.default_record_accessor_klass = JSONAPI::ActiveRecordRecordAccessor + +TestApp.class_eval do + config.active_record.schema_format = :none + + if Rails::VERSION::MAJOR >= 5 + config.active_support.halt_callback_chains_on_return_false = false + config.active_record.time_zone_aware_types = [:time, :datetime] + config.active_record.belongs_to_required_by_default = false + end +end \ No newline at end of file diff --git a/test/support/orm/test_configurator.rb b/test/support/orm/test_configurator.rb new file mode 100644 index 000000000..b58119f5e --- /dev/null +++ b/test/support/orm/test_configurator.rb @@ -0,0 +1,22 @@ +# To specify a different ORM, set ORM environment variable to the name of the orm, like 'sequel'. +ENV["ORM"] ||= "active_record" + +module Orm + class TestConfigurator + attr_accessor :name, :railtie_file + + def record_accessor_class + "JSONAPI::#{name.classify}RecordAccessor".constantize + end + + def models_path + File.expand_path("../fixtures/#{name}", __FILE__) + end + + end + +end + +ORM_TEST_CONFIGURATOR = Orm::TestConfigurator.new + +require_relative "#{ENV["ORM"]}/orm_test_configurator" \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index cb1d47991..d3362be59 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -18,7 +18,9 @@ end end -require 'active_record/railtie' +ENV["ORM"] = "active_record" + +require_relative "support/orm/#{ENV["ORM"]}/initialize" require 'rails/test_help' require 'minitest/mock' require 'jsonapi-resources' @@ -38,7 +40,7 @@ config.json_key_format = :camelized_key end -puts "Testing With RAILS VERSION #{Rails.version}" +puts "Testing With RAILS VERSION #{Rails.version} and #{ENV["ORM"]} ORM" class TestApp < Rails::Application config.eager_load = false @@ -49,14 +51,12 @@ class TestApp < Rails::Application #Raise errors on unsupported parameters config.action_controller.action_on_unpermitted_parameters = :raise - ActiveRecord::Schema.verbose = false - config.active_record.schema_format = :none config.active_support.test_order = :random + ActiveSupport::Deprecation.silenced = true + if Rails::VERSION::MAJOR >= 5 config.active_support.halt_callback_chains_on_return_false = false - config.active_record.time_zone_aware_types = [:time, :datetime] - config.active_record.belongs_to_required_by_default = false end end @@ -190,7 +190,7 @@ def assign_parameters(routes, controller_path, action, parameters, generated_pat def assert_query_count(expected, msg = nil, &block) @queries = [] callback = lambda {|_, _, _, _, payload| @queries.push payload[:sql] } - ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &block) + ActiveSupport::Notifications.subscribed(callback, "sql.#{ENV["ORM"]}", &block) show_queries unless expected == @queries.size assert expected == @queries.size, "Expected #{expected} queries, ran #{@queries.size} queries" @@ -205,7 +205,8 @@ def show_queries TestApp.initialize! -require File.expand_path('../fixtures/active_record', __FILE__) +require_relative "support/orm/#{ENV["ORM"]}/models" +require_relative "support/controllers_resources_processors" module Pets module V1 @@ -412,27 +413,19 @@ class Minitest::Test include Helpers::ValueMatchers include Helpers::FunctionalHelpers include Helpers::ConfigurationHelpers - include ActiveRecord::TestFixtures def run_in_transaction? true end - - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all end class ActiveSupport::TestCase - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all setup do @routes = TestApp.routes end end class ActionDispatch::IntegrationTest - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all def assert_jsonapi_response(expected_status, msg = nil) assert_equal JSONAPI::MEDIA_TYPE, response.content_type @@ -487,7 +480,7 @@ def assert_cacheable_get(action, *args) normal_queries = [] normal_query_callback = lambda {|_, _, _, _, payload| normal_queries.push payload[:sql] } - ActiveSupport::Notifications.subscribed(normal_query_callback, 'sql.active_record') do + ActiveSupport::Notifications.subscribed(normal_query_callback, "sql.#{ENV["ORM"]}") do get action, *args end non_caching_response = json_response_sans_backtraces @@ -518,7 +511,7 @@ def assert_cacheable_get(action, *args) cache_queries = [] cache_query_callback = lambda {|_, _, _, _, payload| cache_queries.push payload[:sql] } cache_activity[phase] = with_resource_caching(cache, cached_resources) do - ActiveSupport::Notifications.subscribed(cache_query_callback, 'sql.active_record') do + ActiveSupport::Notifications.subscribed(cache_query_callback, "sql.#{ENV["ORM"]}") do @controller = nil setup_controller_request_and_response @request.headers.merge!(orig_request_headers.dup) @@ -645,3 +638,5 @@ def unformat(value) end end end + +require_relative "support/orm/#{ENV["ORM"]}/setup" \ No newline at end of file diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index d1dd28d8a..fc918f039 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -567,6 +567,7 @@ def test_key_type_proc end def test_id_attr_deprecation + tmp, ActiveSupport::Deprecation.silenced = ActiveSupport::Deprecation.silenced, false _out, err = capture_io do eval <<-CODE class ProblemResource < JSONAPI::Resource @@ -575,6 +576,8 @@ class ProblemResource < JSONAPI::Resource CODE end assert_match /DEPRECATION WARNING: Id without format is no longer supported. Please remove ids from attributes, or specify a format./, err + ensure + ActiveSupport::Deprecation.silenced = tmp end def test_id_attr_with_format From 7651b0c816a0e157ad2acc7fbe8837f8c608183e Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Fri, 24 Mar 2017 18:39:20 -0700 Subject: [PATCH 6/9] Organize test files under support --- .../{orm => }/active_record/initialize.rb | 0 .../support/{orm => }/active_record/models.rb | 0 .../support/{orm => }/active_record/schema.rb | 0 test/support/{orm => }/active_record/setup.rb | 0 test/{config => support}/database.yml | 0 test/support/models.rb | 350 ------------------ test/support/orm/test_configurator.rb | 22 -- test/support/{orm => }/sequel/initialize.rb | 0 test/support/{orm => }/sequel/models.rb | 0 test/support/{orm => }/sequel/setup.rb | 0 test/test_helper.rb | 8 +- 11 files changed, 5 insertions(+), 375 deletions(-) rename test/support/{orm => }/active_record/initialize.rb (100%) rename test/support/{orm => }/active_record/models.rb (100%) rename test/support/{orm => }/active_record/schema.rb (100%) rename test/support/{orm => }/active_record/setup.rb (100%) rename test/{config => support}/database.yml (100%) delete mode 100644 test/support/models.rb delete mode 100644 test/support/orm/test_configurator.rb rename test/support/{orm => }/sequel/initialize.rb (100%) rename test/support/{orm => }/sequel/models.rb (100%) rename test/support/{orm => }/sequel/setup.rb (100%) diff --git a/test/support/orm/active_record/initialize.rb b/test/support/active_record/initialize.rb similarity index 100% rename from test/support/orm/active_record/initialize.rb rename to test/support/active_record/initialize.rb diff --git a/test/support/orm/active_record/models.rb b/test/support/active_record/models.rb similarity index 100% rename from test/support/orm/active_record/models.rb rename to test/support/active_record/models.rb diff --git a/test/support/orm/active_record/schema.rb b/test/support/active_record/schema.rb similarity index 100% rename from test/support/orm/active_record/schema.rb rename to test/support/active_record/schema.rb diff --git a/test/support/orm/active_record/setup.rb b/test/support/active_record/setup.rb similarity index 100% rename from test/support/orm/active_record/setup.rb rename to test/support/active_record/setup.rb diff --git a/test/config/database.yml b/test/support/database.yml similarity index 100% rename from test/config/database.yml rename to test/support/database.yml diff --git a/test/support/models.rb b/test/support/models.rb deleted file mode 100644 index f323a7662..000000000 --- a/test/support/models.rb +++ /dev/null @@ -1,350 +0,0 @@ -require 'active_record' - -# Here are the models specifically used for fixtures. - - -### MODELS -module FixtureModel - - def self.class_mapping - [ BookComment, Person, AuthorDetail, Post, SpecialPostTag, Comment, Company, Firm, Tag, Section, HairCut, Property, - Customer, BadlyNamedAttributes, Cat, IsoCurrency, ExpenseEntry, Planet, PlanetType, Moon, Crater, Preferences, - Fact, Like, Breed, Book, BookComment, BreedData, Customer, PurchaseOrder, OrderFlag, LineItem, - NumeroTelefone, Category, Picture, Vehicle, Car, Boat, Document, Document, Product, Make, WebPage, - Box, User, Thing, RelatedThing, Question, Answer, Patient, Doctor].inject({}) do |hash, klass| - hash[klass.to_s.demodulize.tableize] = klass - hash - end - end - - class Person < ActiveRecord::Base - has_many :posts, foreign_key: 'author_id' - has_many :comments, foreign_key: 'author_id' - has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception - has_many :vehicles - belongs_to :preferences - belongs_to :hair_cut - has_one :author_detail - - has_and_belongs_to_many :books, join_table: :book_authors - - has_many :even_posts, -> { where('posts.id % 2 = 0') }, class_name: 'Post', foreign_key: 'author_id' - has_many :odd_posts, -> { where('posts.id % 2 = 1') }, class_name: 'Post', foreign_key: 'author_id' - - ### Validations - validates :name, presence: true - validates :date_joined, presence: true - end - - class AuthorDetail < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'person_id' - end - - class Post < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :writer, class_name: 'Person', foreign_key: 'author_id' - has_many :comments - has_and_belongs_to_many :tags, join_table: :posts_tags - has_many :special_post_tags, source: :tag - has_many :special_tags, through: :special_post_tags, source: :tag - belongs_to :section - has_one :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' - - validates :author, presence: true - validates :title, length: { maximum: 35 } - - before_destroy :destroy_callback - - def destroy_callback - if title == "can't destroy me" - errors.add(:title, "can't destroy me") - - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end - end - end - - class SpecialPostTag < ActiveRecord::Base - belongs_to :tag - belongs_to :post - end - - class Comment < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :post - has_and_belongs_to_many :tags, join_table: :comments_tags - end - - class Company < ActiveRecord::Base - end - - class Firm < Company - end - - class Tag < ActiveRecord::Base - has_and_belongs_to_many :posts, join_table: :posts_tags - has_and_belongs_to_many :planets, join_table: :planets_tags - end - - class Section < ActiveRecord::Base - has_many :posts - end - - class HairCut < ActiveRecord::Base - has_many :people - end - - class Property < ActiveRecord::Base - end - - class Customer < ActiveRecord::Base - end - - class BadlyNamedAttributes < ActiveRecord::Base - end - - class Cat < ActiveRecord::Base - end - - class IsoCurrency < ActiveRecord::Base - self.primary_key = :code - # has_many :expense_entries, foreign_key: 'currency_code' - end - - class ExpenseEntry < ActiveRecord::Base - belongs_to :employee, class_name: 'Person', foreign_key: 'employee_id' - belongs_to :iso_currency, foreign_key: 'currency_code' - end - - class Planet < ActiveRecord::Base - has_many :moons - belongs_to :planet_type - - has_and_belongs_to_many :tags, join_table: :planets_tags - - # Test model callback cancelling save - before_save :check_not_pluto - - def check_not_pluto - # Pluto can't be a planet, so cancel the save - if name.downcase == 'pluto' - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end - end - end - - class PlanetType < ActiveRecord::Base - has_many :planets - end - - class Moon < ActiveRecord::Base - belongs_to :planet - - has_many :craters - end - - class Crater < ActiveRecord::Base - self.primary_key = :code - - belongs_to :moon - end - - class Preferences < ActiveRecord::Base - has_one :author, class_name: 'Person', :inverse_of => 'preferences' - end - - class Fact < ActiveRecord::Base - validates :spouse_name, :bio, presence: true - end - - class Like < ActiveRecord::Base - end - - class Breed - - def initialize(id = nil, name = nil) - if id.nil? - @id = $breed_data.new_id - $breed_data.add(self) - else - @id = id - end - @name = name - @errors = ActiveModel::Errors.new(self) - end - - attr_accessor :id, :name - - def destroy - $breed_data.remove(@id) - end - - def valid?(context = nil) - @errors.clear - if name.is_a?(String) && name.length > 0 - return true - else - @errors.add(:name, "can't be blank") - return false - end - end - - def errors - @errors - end - end - - class Book < ActiveRecord::Base - has_many :book_comments - has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" - - has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" - end - - class BookComment < ActiveRecord::Base - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - belongs_to :book - - def self.for_user(current_user) - records = self - # Hide the unapproved comments from people who are not book admins - unless current_user && current_user.book_admin - records = records.where(approved: true) - end - records - end - end - - class BreedData - def initialize - @breeds = {} - end - - def breeds - @breeds - end - - def new_id - @breeds.keys.max + 1 - end - - def add(breed) - @breeds[breed.id] = breed - end - - def remove(id) - @breeds.delete(id) - end - end - - class Customer < ActiveRecord::Base - has_many :purchase_orders - end - - class PurchaseOrder < ActiveRecord::Base - belongs_to :customer - has_many :line_items - has_many :admin_line_items, class_name: 'LineItem', foreign_key: 'purchase_order_id' - - has_and_belongs_to_many :order_flags, join_table: :purchase_orders_order_flags - - has_and_belongs_to_many :admin_order_flags, join_table: :purchase_orders_order_flags, class_name: 'OrderFlag' - end - - class OrderFlag < ActiveRecord::Base - has_and_belongs_to_many :purchase_orders, join_table: :purchase_orders_order_flags - end - - class LineItem < ActiveRecord::Base - belongs_to :purchase_order - end - - class NumeroTelefone < ActiveRecord::Base - end - - class Category < ActiveRecord::Base - end - - class Picture < ActiveRecord::Base - belongs_to :imageable, polymorphic: true - end - - class Vehicle < ActiveRecord::Base - belongs_to :person - end - - class Car < Vehicle - end - - class Boat < Vehicle - end - - class Document < ActiveRecord::Base - has_many :pictures, as: :imageable - end - - class Document::Topic < Document - end - - class Product < ActiveRecord::Base - has_one :picture, as: :imageable - end - - class Make < ActiveRecord::Base - end - - class WebPage < ActiveRecord::Base - end - - class Box < ActiveRecord::Base - has_many :things - end - - class User < ActiveRecord::Base - has_many :things - end - - class Thing < ActiveRecord::Base - belongs_to :box - belongs_to :user - - has_many :related_things, foreign_key: :from_id - has_many :things, through: :related_things, source: :to - end - - class RelatedThing < ActiveRecord::Base - belongs_to :from, class_name: Thing, foreign_key: :from_id - belongs_to :to, class_name: Thing, foreign_key: :to_id - end - - class Question < ActiveRecord::Base - has_one :answer - - def respondent - answer.try(:respondent) - end - end - - class Answer < ActiveRecord::Base - belongs_to :question - belongs_to :respondent, polymorphic: true - end - - class Patient < ActiveRecord::Base - end - - class Doctor < ActiveRecord::Base - end -end \ No newline at end of file diff --git a/test/support/orm/test_configurator.rb b/test/support/orm/test_configurator.rb deleted file mode 100644 index b58119f5e..000000000 --- a/test/support/orm/test_configurator.rb +++ /dev/null @@ -1,22 +0,0 @@ -# To specify a different ORM, set ORM environment variable to the name of the orm, like 'sequel'. -ENV["ORM"] ||= "active_record" - -module Orm - class TestConfigurator - attr_accessor :name, :railtie_file - - def record_accessor_class - "JSONAPI::#{name.classify}RecordAccessor".constantize - end - - def models_path - File.expand_path("../fixtures/#{name}", __FILE__) - end - - end - -end - -ORM_TEST_CONFIGURATOR = Orm::TestConfigurator.new - -require_relative "#{ENV["ORM"]}/orm_test_configurator" \ No newline at end of file diff --git a/test/support/orm/sequel/initialize.rb b/test/support/sequel/initialize.rb similarity index 100% rename from test/support/orm/sequel/initialize.rb rename to test/support/sequel/initialize.rb diff --git a/test/support/orm/sequel/models.rb b/test/support/sequel/models.rb similarity index 100% rename from test/support/orm/sequel/models.rb rename to test/support/sequel/models.rb diff --git a/test/support/orm/sequel/setup.rb b/test/support/sequel/setup.rb similarity index 100% rename from test/support/orm/sequel/setup.rb rename to test/support/sequel/setup.rb diff --git a/test/test_helper.rb b/test/test_helper.rb index d3362be59..6893f2434 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -20,7 +20,7 @@ ENV["ORM"] = "active_record" -require_relative "support/orm/#{ENV["ORM"]}/initialize" +require_relative "support/#{ENV["ORM"]}/initialize" require 'rails/test_help' require 'minitest/mock' require 'jsonapi-resources' @@ -53,6 +53,8 @@ class TestApp < Rails::Application config.active_support.test_order = :random + config.paths["config/database"] = "support/database.yml" + ActiveSupport::Deprecation.silenced = true if Rails::VERSION::MAJOR >= 5 @@ -205,7 +207,7 @@ def show_queries TestApp.initialize! -require_relative "support/orm/#{ENV["ORM"]}/models" +require_relative "support/#{ENV["ORM"]}/models" require_relative "support/controllers_resources_processors" module Pets @@ -639,4 +641,4 @@ def unformat(value) end end -require_relative "support/orm/#{ENV["ORM"]}/setup" \ No newline at end of file +require_relative "support/#{ENV["ORM"]}/setup" \ No newline at end of file From 6b9a594f5a6b04233a53f208de2230f2b12d3de2 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Fri, 24 Mar 2017 18:55:55 -0700 Subject: [PATCH 7/9] Add Sequel schema generation based on ActiveRecord version --- test/support/sequel/schema.rb | 310 ++++++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 test/support/sequel/schema.rb diff --git a/test/support/sequel/schema.rb b/test/support/sequel/schema.rb new file mode 100644 index 000000000..2b76e9b11 --- /dev/null +++ b/test/support/sequel/schema.rb @@ -0,0 +1,310 @@ +require 'active_record' + +ActiveRecord::Schema.verbose = false + +### DATABASE +ActiveRecord::Schema.define do + create_table :people, force: true do |t| + t.string :name + t.string :email + t.datetime :date_joined + t.belongs_to :preferences + t.integer :hair_cut_id, index: true + t.boolean :book_admin, default: false + t.boolean :special, default: false + t.timestamps null: false + end + + create_table :author_details, force: true do |t| + t.integer :person_id + t.string :author_stuff + end + + create_table :posts, force: true do |t| + t.string :title, length: 255 + t.text :body + t.integer :author_id + t.integer :parent_post_id + t.belongs_to :section, index: true + t.timestamps null: false + end + + create_table :comments, force: true do |t| + t.text :body + t.belongs_to :post, index: true + t.integer :author_id + t.timestamps null: false + end + + create_table :companies, force: true do |t| + t.string :type + t.string :name + t.string :address + t.timestamps null: false + end + + create_table :tags, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :sections, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :posts_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :posts_tags, [:post_id, :tag_id], unique: true + + create_table :special_post_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :special_post_tags, [:post_id, :tag_id], unique: true + + create_table :comments_tags, force: true do |t| + t.references :comment, :tag, index: true + end + + create_table :iso_currencies, id: false, force: true do |t| + t.string :code, limit: 3, null: false + t.string :name + t.string :country_name + t.string :minor_unit + t.timestamps null: false + end + add_index :iso_currencies, :code, unique: true + + create_table :expense_entries, force: true do |t| + t.string :currency_code, limit: 3, null: false + t.integer :employee_id, null: false + t.decimal :cost, precision: 12, scale: 4, null: false + t.date :transaction_date + t.timestamps null: false + end + + create_table :planets, force: true do |t| + t.string :name + t.string :description + t.integer :planet_type_id + end + + create_table :planets_tags, force: true do |t| + t.references :planet, :tag, index: true + end + add_index :planets_tags, [:planet_id, :tag_id], unique: true + + create_table :planet_types, force: true do |t| + t.string :name + end + + create_table :moons, force: true do |t| + t.string :name + t.string :description + t.integer :planet_id + t.timestamps null: false + end + + create_table :craters, id: false, force: true do |t| + t.string :code + t.string :description + t.integer :moon_id + t.timestamps null: false + end + + create_table :preferences, force: true do |t| + t.integer :person_id + t.boolean :advanced_mode, default: false + t.timestamps null: false + end + + create_table :facts, force: true do |t| + t.integer :person_id + t.string :spouse_name + t.text :bio + t.float :quality_rating + t.decimal :salary, precision: 12, scale: 2 + t.datetime :date_time_joined + t.date :birthday + t.time :bedtime + t.binary :photo, limit: 1.kilobyte + t.boolean :cool + t.timestamps null: false + end + + create_table :books, force: true do |t| + t.string :title + t.string :isbn + t.boolean :banned, default: false + t.timestamps null: false + end + + create_table :book_authors, force: true do |t| + t.integer :book_id + t.integer :person_id + end + + create_table :book_comments, force: true do |t| + t.text :body + t.belongs_to :book, index: true + t.integer :author_id + t.boolean :approved, default: true + t.timestamps null: false + end + + create_table :customers, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :purchase_orders, force: true do |t| + t.date :order_date + t.date :requested_delivery_date + t.date :delivery_date + t.integer :customer_id + t.string :delivery_name + t.string :delivery_address_1 + t.string :delivery_address_2 + t.string :delivery_city + t.string :delivery_state + t.string :delivery_postal_code + t.float :delivery_fee + t.float :tax + t.float :total + t.timestamps null: false + end + + create_table :order_flags, force: true do |t| + t.string :name + end + + create_table :purchase_orders_order_flags, force: true do |t| + t.references :purchase_order, :order_flag, index: true + end + add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" + + create_table :line_items, force: true do |t| + t.integer :purchase_order_id + t.string :part_number + t.string :quantity + t.float :item_cost + t.timestamps null: false + end + + create_table :hair_cuts, force: true do |t| + t.string :style + end + + create_table :numeros_telefone, force: true do |t| + t.string :numero_telefone + t.timestamps null: false + end + + create_table :categories, force: true do |t| + t.string :name + t.string :status, limit: 10 + t.timestamps null: false + end + + create_table :pictures, force: true do |t| + t.string :name + t.integer :imageable_id + t.string :imageable_type + t.timestamps null: false + end + + create_table :documents, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :products, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :vehicles, force: true do |t| + t.string :type + t.string :make + t.string :model + t.string :length_at_water_line + t.string :drive_layout + t.string :serial_number + t.integer :person_id + t.timestamps null: false + end + + create_table :makes, force: true do |t| + t.string :model + t.timestamps null: false + end + + # special cases - fields that look like they should be reserved names + create_table :hrefs, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :links, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :web_pages, force: true do |t| + t.string :href + t.string :link + t.timestamps null: false + end + + create_table :questionables, force: true do |t| + t.timestamps null: false + end + + create_table :boxes, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :things, force: true do |t| + t.string :name + t.references :user + t.references :box + + t.timestamps null: false + end + + create_table :users, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :related_things, force: true do |t| + t.string :name + t.references :from, references: :thing + t.references :to, references: :thing + + t.timestamps null: false + end + + create_table :questions, force: true do |t| + t.string :text + end + + create_table :answers, force: true do |t| + t.references :question + t.integer :respondent_id + t.string :respondent_type + t.string :text + end + + create_table :patients, force: true do |t| + t.string :name + end + + create_table :doctors, force: true do |t| + t.string :name + end + + # special cases +end \ No newline at end of file From 57b587918a3c22daa9a216a6c66da20d582b3115 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Mon, 27 Mar 2017 04:30:25 -0700 Subject: [PATCH 8/9] Move fixtures --- test/{ => support/database}/fixtures/answers.yml | 0 test/{ => support/database}/fixtures/author_details.yml | 0 test/{ => support/database}/fixtures/book_authors.yml | 0 test/{ => support/database}/fixtures/book_comments.yml | 0 test/{ => support/database}/fixtures/books.yml | 0 test/{ => support/database}/fixtures/boxes.yml | 0 test/{ => support/database}/fixtures/categories.yml | 0 test/{ => support/database}/fixtures/comments.yml | 0 test/{ => support/database}/fixtures/comments_tags.yml | 0 test/{ => support/database}/fixtures/companies.yml | 0 test/{ => support/database}/fixtures/craters.yml | 0 test/{ => support/database}/fixtures/customers.yml | 0 test/{ => support/database}/fixtures/doctors.yml | 0 test/{ => support/database}/fixtures/documents.yml | 0 test/{ => support/database}/fixtures/expense_entries.yml | 0 test/{ => support/database}/fixtures/facts.yml | 0 test/{ => support/database}/fixtures/hair_cuts.yml | 0 test/{ => support/database}/fixtures/iso_currencies.yml | 0 test/{ => support/database}/fixtures/line_items.yml | 0 test/{ => support/database}/fixtures/makes.yml | 0 test/{ => support/database}/fixtures/moons.yml | 0 test/{ => support/database}/fixtures/numeros_telefone.yml | 0 test/{ => support/database}/fixtures/order_flags.yml | 0 test/{ => support/database}/fixtures/patients.yml | 0 test/{ => support/database}/fixtures/people.yml | 0 test/{ => support/database}/fixtures/pictures.yml | 0 test/{ => support/database}/fixtures/planet_types.yml | 0 test/{ => support/database}/fixtures/planets.yml | 0 test/{ => support/database}/fixtures/posts.yml | 0 test/{ => support/database}/fixtures/posts_tags.yml | 0 test/{ => support/database}/fixtures/preferences.yml | 0 test/{ => support/database}/fixtures/products.yml | 0 test/{ => support/database}/fixtures/purchase_orders.yml | 0 test/{ => support/database}/fixtures/questions.yml | 0 test/{ => support/database}/fixtures/related_things.yml | 0 test/{ => support/database}/fixtures/sections.yml | 0 test/{ => support/database}/fixtures/tags.yml | 0 test/{ => support/database}/fixtures/things.yml | 0 test/{ => support/database}/fixtures/users.yml | 0 test/{ => support/database}/fixtures/vehicles.yml | 0 test/{ => support/database}/fixtures/web_pages.yml | 0 41 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => support/database}/fixtures/answers.yml (100%) rename test/{ => support/database}/fixtures/author_details.yml (100%) rename test/{ => support/database}/fixtures/book_authors.yml (100%) rename test/{ => support/database}/fixtures/book_comments.yml (100%) rename test/{ => support/database}/fixtures/books.yml (100%) rename test/{ => support/database}/fixtures/boxes.yml (100%) rename test/{ => support/database}/fixtures/categories.yml (100%) rename test/{ => support/database}/fixtures/comments.yml (100%) rename test/{ => support/database}/fixtures/comments_tags.yml (100%) rename test/{ => support/database}/fixtures/companies.yml (100%) rename test/{ => support/database}/fixtures/craters.yml (100%) rename test/{ => support/database}/fixtures/customers.yml (100%) rename test/{ => support/database}/fixtures/doctors.yml (100%) rename test/{ => support/database}/fixtures/documents.yml (100%) rename test/{ => support/database}/fixtures/expense_entries.yml (100%) rename test/{ => support/database}/fixtures/facts.yml (100%) rename test/{ => support/database}/fixtures/hair_cuts.yml (100%) rename test/{ => support/database}/fixtures/iso_currencies.yml (100%) rename test/{ => support/database}/fixtures/line_items.yml (100%) rename test/{ => support/database}/fixtures/makes.yml (100%) rename test/{ => support/database}/fixtures/moons.yml (100%) rename test/{ => support/database}/fixtures/numeros_telefone.yml (100%) rename test/{ => support/database}/fixtures/order_flags.yml (100%) rename test/{ => support/database}/fixtures/patients.yml (100%) rename test/{ => support/database}/fixtures/people.yml (100%) rename test/{ => support/database}/fixtures/pictures.yml (100%) rename test/{ => support/database}/fixtures/planet_types.yml (100%) rename test/{ => support/database}/fixtures/planets.yml (100%) rename test/{ => support/database}/fixtures/posts.yml (100%) rename test/{ => support/database}/fixtures/posts_tags.yml (100%) rename test/{ => support/database}/fixtures/preferences.yml (100%) rename test/{ => support/database}/fixtures/products.yml (100%) rename test/{ => support/database}/fixtures/purchase_orders.yml (100%) rename test/{ => support/database}/fixtures/questions.yml (100%) rename test/{ => support/database}/fixtures/related_things.yml (100%) rename test/{ => support/database}/fixtures/sections.yml (100%) rename test/{ => support/database}/fixtures/tags.yml (100%) rename test/{ => support/database}/fixtures/things.yml (100%) rename test/{ => support/database}/fixtures/users.yml (100%) rename test/{ => support/database}/fixtures/vehicles.yml (100%) rename test/{ => support/database}/fixtures/web_pages.yml (100%) diff --git a/test/fixtures/answers.yml b/test/support/database/fixtures/answers.yml similarity index 100% rename from test/fixtures/answers.yml rename to test/support/database/fixtures/answers.yml diff --git a/test/fixtures/author_details.yml b/test/support/database/fixtures/author_details.yml similarity index 100% rename from test/fixtures/author_details.yml rename to test/support/database/fixtures/author_details.yml diff --git a/test/fixtures/book_authors.yml b/test/support/database/fixtures/book_authors.yml similarity index 100% rename from test/fixtures/book_authors.yml rename to test/support/database/fixtures/book_authors.yml diff --git a/test/fixtures/book_comments.yml b/test/support/database/fixtures/book_comments.yml similarity index 100% rename from test/fixtures/book_comments.yml rename to test/support/database/fixtures/book_comments.yml diff --git a/test/fixtures/books.yml b/test/support/database/fixtures/books.yml similarity index 100% rename from test/fixtures/books.yml rename to test/support/database/fixtures/books.yml diff --git a/test/fixtures/boxes.yml b/test/support/database/fixtures/boxes.yml similarity index 100% rename from test/fixtures/boxes.yml rename to test/support/database/fixtures/boxes.yml diff --git a/test/fixtures/categories.yml b/test/support/database/fixtures/categories.yml similarity index 100% rename from test/fixtures/categories.yml rename to test/support/database/fixtures/categories.yml diff --git a/test/fixtures/comments.yml b/test/support/database/fixtures/comments.yml similarity index 100% rename from test/fixtures/comments.yml rename to test/support/database/fixtures/comments.yml diff --git a/test/fixtures/comments_tags.yml b/test/support/database/fixtures/comments_tags.yml similarity index 100% rename from test/fixtures/comments_tags.yml rename to test/support/database/fixtures/comments_tags.yml diff --git a/test/fixtures/companies.yml b/test/support/database/fixtures/companies.yml similarity index 100% rename from test/fixtures/companies.yml rename to test/support/database/fixtures/companies.yml diff --git a/test/fixtures/craters.yml b/test/support/database/fixtures/craters.yml similarity index 100% rename from test/fixtures/craters.yml rename to test/support/database/fixtures/craters.yml diff --git a/test/fixtures/customers.yml b/test/support/database/fixtures/customers.yml similarity index 100% rename from test/fixtures/customers.yml rename to test/support/database/fixtures/customers.yml diff --git a/test/fixtures/doctors.yml b/test/support/database/fixtures/doctors.yml similarity index 100% rename from test/fixtures/doctors.yml rename to test/support/database/fixtures/doctors.yml diff --git a/test/fixtures/documents.yml b/test/support/database/fixtures/documents.yml similarity index 100% rename from test/fixtures/documents.yml rename to test/support/database/fixtures/documents.yml diff --git a/test/fixtures/expense_entries.yml b/test/support/database/fixtures/expense_entries.yml similarity index 100% rename from test/fixtures/expense_entries.yml rename to test/support/database/fixtures/expense_entries.yml diff --git a/test/fixtures/facts.yml b/test/support/database/fixtures/facts.yml similarity index 100% rename from test/fixtures/facts.yml rename to test/support/database/fixtures/facts.yml diff --git a/test/fixtures/hair_cuts.yml b/test/support/database/fixtures/hair_cuts.yml similarity index 100% rename from test/fixtures/hair_cuts.yml rename to test/support/database/fixtures/hair_cuts.yml diff --git a/test/fixtures/iso_currencies.yml b/test/support/database/fixtures/iso_currencies.yml similarity index 100% rename from test/fixtures/iso_currencies.yml rename to test/support/database/fixtures/iso_currencies.yml diff --git a/test/fixtures/line_items.yml b/test/support/database/fixtures/line_items.yml similarity index 100% rename from test/fixtures/line_items.yml rename to test/support/database/fixtures/line_items.yml diff --git a/test/fixtures/makes.yml b/test/support/database/fixtures/makes.yml similarity index 100% rename from test/fixtures/makes.yml rename to test/support/database/fixtures/makes.yml diff --git a/test/fixtures/moons.yml b/test/support/database/fixtures/moons.yml similarity index 100% rename from test/fixtures/moons.yml rename to test/support/database/fixtures/moons.yml diff --git a/test/fixtures/numeros_telefone.yml b/test/support/database/fixtures/numeros_telefone.yml similarity index 100% rename from test/fixtures/numeros_telefone.yml rename to test/support/database/fixtures/numeros_telefone.yml diff --git a/test/fixtures/order_flags.yml b/test/support/database/fixtures/order_flags.yml similarity index 100% rename from test/fixtures/order_flags.yml rename to test/support/database/fixtures/order_flags.yml diff --git a/test/fixtures/patients.yml b/test/support/database/fixtures/patients.yml similarity index 100% rename from test/fixtures/patients.yml rename to test/support/database/fixtures/patients.yml diff --git a/test/fixtures/people.yml b/test/support/database/fixtures/people.yml similarity index 100% rename from test/fixtures/people.yml rename to test/support/database/fixtures/people.yml diff --git a/test/fixtures/pictures.yml b/test/support/database/fixtures/pictures.yml similarity index 100% rename from test/fixtures/pictures.yml rename to test/support/database/fixtures/pictures.yml diff --git a/test/fixtures/planet_types.yml b/test/support/database/fixtures/planet_types.yml similarity index 100% rename from test/fixtures/planet_types.yml rename to test/support/database/fixtures/planet_types.yml diff --git a/test/fixtures/planets.yml b/test/support/database/fixtures/planets.yml similarity index 100% rename from test/fixtures/planets.yml rename to test/support/database/fixtures/planets.yml diff --git a/test/fixtures/posts.yml b/test/support/database/fixtures/posts.yml similarity index 100% rename from test/fixtures/posts.yml rename to test/support/database/fixtures/posts.yml diff --git a/test/fixtures/posts_tags.yml b/test/support/database/fixtures/posts_tags.yml similarity index 100% rename from test/fixtures/posts_tags.yml rename to test/support/database/fixtures/posts_tags.yml diff --git a/test/fixtures/preferences.yml b/test/support/database/fixtures/preferences.yml similarity index 100% rename from test/fixtures/preferences.yml rename to test/support/database/fixtures/preferences.yml diff --git a/test/fixtures/products.yml b/test/support/database/fixtures/products.yml similarity index 100% rename from test/fixtures/products.yml rename to test/support/database/fixtures/products.yml diff --git a/test/fixtures/purchase_orders.yml b/test/support/database/fixtures/purchase_orders.yml similarity index 100% rename from test/fixtures/purchase_orders.yml rename to test/support/database/fixtures/purchase_orders.yml diff --git a/test/fixtures/questions.yml b/test/support/database/fixtures/questions.yml similarity index 100% rename from test/fixtures/questions.yml rename to test/support/database/fixtures/questions.yml diff --git a/test/fixtures/related_things.yml b/test/support/database/fixtures/related_things.yml similarity index 100% rename from test/fixtures/related_things.yml rename to test/support/database/fixtures/related_things.yml diff --git a/test/fixtures/sections.yml b/test/support/database/fixtures/sections.yml similarity index 100% rename from test/fixtures/sections.yml rename to test/support/database/fixtures/sections.yml diff --git a/test/fixtures/tags.yml b/test/support/database/fixtures/tags.yml similarity index 100% rename from test/fixtures/tags.yml rename to test/support/database/fixtures/tags.yml diff --git a/test/fixtures/things.yml b/test/support/database/fixtures/things.yml similarity index 100% rename from test/fixtures/things.yml rename to test/support/database/fixtures/things.yml diff --git a/test/fixtures/users.yml b/test/support/database/fixtures/users.yml similarity index 100% rename from test/fixtures/users.yml rename to test/support/database/fixtures/users.yml diff --git a/test/fixtures/vehicles.yml b/test/support/database/fixtures/vehicles.yml similarity index 100% rename from test/fixtures/vehicles.yml rename to test/support/database/fixtures/vehicles.yml diff --git a/test/fixtures/web_pages.yml b/test/support/database/fixtures/web_pages.yml similarity index 100% rename from test/fixtures/web_pages.yml rename to test/support/database/fixtures/web_pages.yml From fab71ed542035085b7d307c1a789a0af09d8c24b Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Sat, 15 Apr 2017 16:31:38 -0700 Subject: [PATCH 9/9] 70% of specs for Sequel ORM support working --- Rakefile | 5 + jsonapi-resources.gemspec | 2 + lib/jsonapi/active_record_record_accessor.rb | 343 +++---------- lib/jsonapi/acts_as_resource_controller.rb | 5 +- lib/jsonapi/cached_resource_fragment.rb | 5 +- lib/jsonapi/record_accessor.rb | 431 ++++++++++++++-- lib/jsonapi/resource.rb | 58 ++- lib/jsonapi/sequel_record_accessor.rb | 471 ++++++------------ .../reflect_create_and_delete_benchmark.rb | 6 +- .../reflect_update_relationships_benchmark.rb | 6 +- test/benchmark/request_benchmark.rb | 2 +- test/controllers/controller_test.rb | 169 ++++--- test/helpers/configuration_helpers.rb | 5 +- test/helpers/record_accessor_helpers.rb | 21 + test/integration/requests/request_test.rb | 20 +- .../setup.rb => active_record/app_config.rb} | 0 test/support/active_record/import_schema.rb | 11 + test/support/active_record/models.rb | 32 +- test/support/active_record/rollback.rb | 21 + test/support/active_record/schema.rb | 310 ------------ test/support/active_record/setup.rb | 29 -- .../controllers_resources_processors.rb | 31 +- .../{database.yml => database/config.yml} | 0 test/support/database/generator.rb | 359 +++++++++++++ test/support/inflections.rb | 6 + test/support/sequel/app_config.rb | 1 + test/support/sequel/import_schema.rb | 6 + test/support/sequel/models.rb | 133 ++--- test/support/sequel/rollback.rb | 22 + test/support/sequel/schema.rb | 310 ------------ test/test_helper.rb | 62 ++- test/unit/resource/resource_test.rb | 53 +- .../serializer/polymorphic_serializer_test.rb | 2 +- test/unit/serializer/serializer_test.rb | 24 +- 34 files changed, 1387 insertions(+), 1574 deletions(-) create mode 100644 test/helpers/record_accessor_helpers.rb rename test/support/{sequel/setup.rb => active_record/app_config.rb} (100%) create mode 100644 test/support/active_record/import_schema.rb create mode 100644 test/support/active_record/rollback.rb delete mode 100644 test/support/active_record/schema.rb delete mode 100644 test/support/active_record/setup.rb rename test/support/{database.yml => database/config.yml} (100%) create mode 100644 test/support/database/generator.rb create mode 100644 test/support/inflections.rb create mode 100644 test/support/sequel/app_config.rb create mode 100644 test/support/sequel/import_schema.rb create mode 100644 test/support/sequel/rollback.rb delete mode 100644 test/support/sequel/schema.rb diff --git a/Rakefile b/Rakefile index 7c629c8a6..c18491c1a 100644 --- a/Rakefile +++ b/Rakefile @@ -15,4 +15,9 @@ namespace :test do Rake::TestTask.new(:benchmark) do |t| t.pattern = 'test/benchmark/*_benchmark.rb' end + + desc "Refresh dump.sql from fixtures and schema." + task :refresh_dump do + require_relative 'test/support/database/generator' + end end diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index 44ed00afd..557883627 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -29,6 +29,8 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'concurrent-ruby-ext' spec.add_development_dependency 'sequel' spec.add_development_dependency 'sequel-rails' + spec.add_development_dependency 'sequel_polymorphic' + spec.add_development_dependency 'fixture_dependencies' spec.add_development_dependency 'activerecord', '>= 4.1' spec.add_dependency 'activesupport', '>= 4.1' spec.add_dependency 'railties', '>= 4.1' diff --git a/lib/jsonapi/active_record_record_accessor.rb b/lib/jsonapi/active_record_record_accessor.rb index 8b710a444..520c012e0 100644 --- a/lib/jsonapi/active_record_record_accessor.rb +++ b/lib/jsonapi/active_record_record_accessor.rb @@ -4,136 +4,78 @@ module JSONAPI class ActiveRecordRecordAccessor < RecordAccessor # RecordAccessor methods - def transaction - ActiveRecord::Base.transaction do - yield - end - end - - def rollback_transaction - fail ActiveRecord::Rollback - end - - def model_error_messages(model) - model.errors.messages - end - - def model_base_class - ActiveRecord::Base - end + class << self - def delete_restriction_error_class - ActiveRecord::DeleteRestrictionError - end - - def record_not_found_error_class - ActiveRecord::RecordNotFound - end - - def association_model_class_name(from_model, relationship_name) - (reflect = from_model.reflect_on_association(relationship_name)) && reflect.class_name - end - - def find_resource(filters, options = {}) - if options[:caching] && options[:caching][:cache_serializer_output] - find_serialized_with_caching(filters, options[:caching][:serializer], options) - else - _resource_klass.resources_for(find_records(filters, options), options[:context]) + def transaction + ActiveRecord::Base.transaction do + yield + end end - end - def find_resource_by_key(key, options = {}) - if options[:caching] && options[:caching][:cache_serializer_output] - find_by_key_serialized_with_caching(key, options[:caching][:serializer], options) - else - records = find_records({ _resource_klass._primary_key => key }, options.except(:paginator, :sort_criteria)) - model = records.first - fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil? - _resource_klass.resource_for(model, options[:context]) + def rollback_transaction + fail ActiveRecord::Rollback end - end - def find_resources_by_keys(keys, options = {}) - records = records(options) - records = apply_includes(records, options) - records = records.where({ _resource_klass._primary_key => keys }) - - _resource_klass.resources_for(records, options[:context]) - end - - def find_count(filters, options = {}) - count_records(filter_records(filters, options)) - end + def model_error_messages(model) + model.errors.messages + end - def related_resource(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] + def valid?(model, validation_context) + model.valid?(validation_context) + end - if relationship.polymorphic? - associated_model = records_for_relationship(resource, relationship_name, options) - resource_klass = resource.class.resource_klass_for_model(associated_model) if associated_model - return resource_klass.new(associated_model, resource.context) if resource_klass && associated_model - else - resource_klass = relationship.resource_klass - if resource_klass - associated_model = records_for_relationship(resource, relationship_name, options) - return associated_model ? resource_klass.new(associated_model, resource.context) : nil - end + def save(model, options={}) + method = options[:raise_on_failure] ? :save! : :save + model.public_send(method, options.slice(:validate)) end - end - def related_resources(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - relationship_resource_klass = relationship.resource_klass + def destroy(model, options={}) + model.destroy + end - if options[:caching] && options[:caching][:cache_serializer_output] - scope = relationship_resource_klass._record_accessor.records_for_relationship(resource, relationship_name, options) - relationship_resource_klass._record_accessor.find_serialized_with_caching(scope, options[:caching][:serializer], options) - else - records = records_for_relationship(resource, relationship_name, options) - return records.collect do |record| - klass = relationship.polymorphic? ? resource.class.resource_klass_for_model(record) : relationship_resource_klass - klass.new(record, resource.context) - end + def delete_relationship(model, relationship_name, id) + model.public_send(relationship_name).delete(id) end - end - def count_for_relationship(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] + def reload(model) + model.reload + end - context = resource.context + def model_base_class + ActiveRecord::Base + end - relation_name = relationship.relation_name(context: context) - records = records_for(resource, relation_name) + def delete_restriction_error_class + ActiveRecord::DeleteRestrictionError + end - resource_klass = relationship.resource_klass + def record_not_found_error_class + ActiveRecord::RecordNotFound + end - filters = options.fetch(:filters, {}) - unless filters.nil? || filters.empty? - records = resource_klass._record_accessor.apply_filters(records, filters, options) + def find_in_association(model, association_name, ids) + primary_key = model.class.reflections[association_name.to_s].klass.primary_key + model.public_send(association_name).where(primary_key => ids) end - records.count(:all) - end + def add_to_association(model, association_name, association_model) + model.public_send(association_name) << association_model + end - def foreign_key(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] + def association_model_class_name(from_model, relationship_name) + (reflect = from_model.reflect_on_association(relationship_name)) && reflect.class_name + end - if relationship.belongs_to? - resource._model.method(relationship.foreign_key).call - else - records = records_for_relationship(resource, relationship_name, options) - return nil if records.nil? - records.public_send(relationship.resource_klass._primary_key) + def set_primary_keys(model, relationship, value) + model.method("#{relationship.foreign_key}=").call(value) end + end - def foreign_keys(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - records = records_for_relationship(resource, relationship_name, options) - records.collect do |record| - record.public_send(relationship.resource_klass._primary_key) - end + # In AR, the .all command will return a chainable relation in which you can attach limit, offset, where, etc. + def model_class_relation + _resource_klass._model_class.all end # protected-ish methods left public for tests and what not @@ -141,6 +83,8 @@ def foreign_keys(resource, relationship_name, options = {}) def find_serialized_with_caching(filters_or_source, serializer, options = {}) if filters_or_source.is_a?(ActiveRecord::Relation) return cached_resources_for(filters_or_source, serializer, options) + # TODO - if we are already in ActiveRecordRecordAccessor, then _resource_klass._model_class + # will essentially always support ActiveRelation, right? Maybe we should get rid of this check. elsif _resource_klass._model_class.respond_to?(:all) && _resource_klass._model_class.respond_to?(:arel_table) records = find_records(filters_or_source, options.except(:include_directives)) return cached_resources_for(records, serializer, options) @@ -151,93 +95,6 @@ def find_serialized_with_caching(filters_or_source, serializer, options = {}) end end - def find_by_key_serialized_with_caching(key, serializer, options = {}) - if _resource_klass._model_class.respond_to?(:all) && _resource_klass._model_class.respond_to?(:arel_table) - results = find_serialized_with_caching({ _resource_klass._primary_key => key }, serializer, options) - result = results.first - fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil? - return result - else - # :nocov: - warn('Caching enabled on model that does not support ActiveRelation') - # :nocov: - end - end - - def records_for_relationship(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - - context = resource.context - - relation_name = relationship.relation_name(context: context) - records = records_for(resource, relation_name) - - resource_klass = relationship.resource_klass - - filters = options.fetch(:filters, {}) - unless filters.nil? || filters.empty? - records = resource_klass._record_accessor.apply_filters(records, filters, options) - end - - sort_criteria = options.fetch(:sort_criteria, {}) - order_options = relationship.resource_klass.construct_order_options(sort_criteria) - records = apply_sort(records, order_options, context) - - paginator = options[:paginator] - if paginator - records = apply_pagination(records, paginator, order_options) - end - - records - end - - # Implement self.records on the resource if you want to customize the relation for - # finder methods (find, find_by_key, find_serialized_with_caching) - def records(_options = {}) - if defined?(_resource_klass.records) - _resource_klass.records(_options) - else - _resource_klass._model_class.all - end - end - - # Implement records_for on the resource to customize how the associated records - # are fetched for a model. Particularly helpful for authorization. - def records_for(resource, relation_name) - if resource.respond_to?(:records_for) - return resource.records_for(relation_name) - end - - relationship = resource.class._relationships[relation_name] - - if relationship.is_a?(JSONAPI::Relationship::ToMany) - if resource.respond_to?(:"records_for_#{relation_name}") - return resource.method(:"records_for_#{relation_name}").call - end - else - if resource.respond_to?(:"record_for_#{relation_name}") - return resource.method(:"record_for_#{relation_name}").call - end - end - - resource._model.public_send(relation_name) - end - - def apply_includes(records, options = {}) - include_directives = options[:include_directives] - if include_directives - model_includes = resolve_relationship_names_to_relations(_resource_klass, include_directives.model_includes, options) - records = records.includes(model_includes) - end - - records - end - - def apply_pagination(records, paginator, order_options) - records = paginator.apply(records, order_options) if paginator - records - end - def apply_sort(records, order_options, context = {}) if defined?(_resource_klass.apply_sort) _resource_klass.apply_sort(records, order_options, context) @@ -258,7 +115,6 @@ def apply_sort(records, order_options, context = {}) end end end - records end end @@ -300,72 +156,31 @@ def apply_filter(records, filter, value, options = {}) end end - # Assumes ActiveRecord's counting. Override if you need a different counting method - def count_records(records) - records.count(:all) - end - - def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {}) - case model_includes - when Array - return model_includes.map do |value| - resolve_relationship_names_to_relations(resource_klass, value, options) - end - 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) - end - return model_includes - when Symbol - relationship = resource_klass._relationships[model_includes] - return relationship.relation_name(options) - end - end - - def apply_filters(records, filters, options = {}) + def apply_filters_to_many_relationships(records, to_many_filters, options) required_includes = [] - - if filters - filters.each do |filter, value| - if _resource_klass._relationships.include?(filter) - if _resource_klass._relationships[filter].belongs_to? - records = apply_filter(records, _resource_klass._relationships[filter].foreign_key, value, options) - else - required_includes.push(filter.to_s) - records = apply_filter(records, "#{_resource_klass._relationships[filter].table_name}.#{_resource_klass._relationships[filter].primary_key}", value, options) - end - else - records = apply_filter(records, filter, value, options) - end - end + to_many_filters.each do |filter, value| + relationship = _resource_klass._relationships[filter] + required_includes << filter.to_s + records = apply_filter(records, "#{relationship.table_name}.#{relationship.primary_key}", value, options) end - if required_includes.any? - records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(_resource_klass, required_includes, force_eager_load: true))) - end + records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(_resource_klass, required_includes, force_eager_load: true))) records end - def filter_records(filters, options, records = records(options)) - records = apply_filters(records, filters, options) - apply_includes(records, options) - end - - def sort_records(records, order_options, context = {}) - apply_sort(records, order_options, context) + # ActiveRecord requires :all to be specified + def count_records(records) + records.count(:all) end def cached_resources_for(records, serializer, options) if _resource_klass.caching? t = _resource_klass._model_class.arel_table - cache_ids = pluck_arel_attributes(records, t[_resource_klass._primary_key], t[_resource_klass._cache_field]) + cache_ids = pluck_attributes(records, t[_resource_klass._primary_key], t[_resource_klass._cache_field]) resources = CachedResourceFragment.fetch_fragments(_resource_klass, serializer, options[:context], cache_ids) else - resources = _resource_klass.resources_for(records, options[:context]).map { |r| [r.id, r] }.to_h + resources = resources_for(records, options[:context]).map { |r| [r.id, r] }.to_h end preload_included_fragments(resources, records, serializer, options) @@ -373,28 +188,6 @@ def cached_resources_for(records, serializer, options) resources.values end - def find_records(filters, options = {}) - if defined?(_resource_klass.find_records) - ActiveSupport::Deprecation.warn "In #{_resource_klass.name} you overrode `find_records`. "\ - "`find_records` has been deprecated in favor of using `apply` "\ - "and `verify` callables on the filter." - - _resource_klass.find_records(filters, options) - else - context = options[:context] - - records = filter_records(filters, options) - - sort_criteria = options.fetch(:sort_criteria) { [] } - order_options = _resource_klass.construct_order_options(sort_criteria) - records = sort_records(records, order_options, context) - - records = apply_pagination(records, options[:paginator], order_options) - - records - end - end - def preload_included_fragments(resources, records, serializer, options) return if resources.empty? res_ids = resources.keys @@ -428,7 +221,7 @@ def preload_included_fragments(resources, records, serializer, options) relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author table = nil # people assocs_path = [] # [ :posts, :approved_comments, :author ] - ar_hash = nil # { :posts => { :approved_comments => :author } } + joins_hash = nil # { :posts => { :approved_comments => :author } } # For each step on the path, figure out what the actual table name/alias in the join # will be, and include the primary key of that table in our list of fields to select @@ -443,10 +236,10 @@ def preload_included_fragments(resources, records, serializer, options) end assocs_path << relationship.relation_name(options).to_sym # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }} - ar_hash = assocs_path.reverse.reduce { |memo, step| { step => memo } } + joins_hash = assocs_path.reverse.reduce { |memo, step| { step => memo } } # We can't just look up the table name from the resource class, because Arel could # have used a table alias if the relation includes a self-reference. - join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node| + join_source = relation.joins(joins_hash).arel.source.right.reverse.find do |arel_node| arel_node.is_a?(Arel::Nodes::InnerJoin) end table = join_source.left @@ -470,7 +263,7 @@ def preload_included_fragments(resources, records, serializer, options) end pluck_attrs << table[klass._cache_field] if klass.caching? - relation = relation.joins(ar_hash) + relation = relation.joins(joins_hash) if relationship.is_a?(JSONAPI::Relationship::ToMany) # Rails doesn't include order clauses in `joins`, so we have to add that manually here. # FIXME Should find a better way to reflect on relationship ordering. :-( @@ -478,10 +271,9 @@ def preload_included_fragments(resources, records, serializer, options) end # [[post id, comment id, author id, author updated_at], ...] - id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs) + id_rows = pluck_attributes(relation.joins(joins_hash), *pluck_attrs) target_resources[klass.name] ||= {} - if klass.caching? sub_cache_ids = id_rows .map { |row| row.last(2) } @@ -512,10 +304,11 @@ def preload_included_fragments(resources, records, serializer, options) end end end + end end - def pluck_arel_attributes(relation, *attrs) + def pluck_attributes(relation, *attrs) conn = relation.connection quoted_attrs = attrs.map do |attr| quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name) diff --git a/lib/jsonapi/acts_as_resource_controller.rb b/lib/jsonapi/acts_as_resource_controller.rb index 6180a6607..90ac7bfa9 100644 --- a/lib/jsonapi/acts_as_resource_controller.rb +++ b/lib/jsonapi/acts_as_resource_controller.rb @@ -96,6 +96,7 @@ def process_request process_operation(op) end rescue => e + debugger if ENV["RAISE"] handle_exceptions(e) end end @@ -114,7 +115,7 @@ def process_request def run_in_transaction(transactional) if transactional run_callbacks :transaction do - resource_klass._record_accessor.transaction do + resource_klass._record_accessor_klass.transaction do yield end end @@ -126,7 +127,7 @@ def run_in_transaction(transactional) def rollback_transaction(transactional) if transactional run_callbacks :rollback do - resource_klass._record_accessor.rollback_transaction + resource_klass._record_accessor_klass.rollback_transaction end end end diff --git a/lib/jsonapi/cached_resource_fragment.rb b/lib/jsonapi/cached_resource_fragment.rb index a8ed301b2..495dc45a4 100644 --- a/lib/jsonapi/cached_resource_fragment.rb +++ b/lib/jsonapi/cached_resource_fragment.rb @@ -8,11 +8,10 @@ def self.fetch_fragments(resource_klass, serializer, context, cache_ids) results = self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids) - miss_ids = results.select{|k,v| v.nil? }.keys + miss_ids = results.select{ |k,v| v.nil? }.keys unless miss_ids.empty? find_filters = {resource_klass._primary_key => miss_ids.uniq} - find_options = {context: context} - resource_klass.find(find_filters, find_options).each do |resource| + resource_klass.find(find_filters, context: context).each do |resource| (id, cr) = write(resource_klass, resource, serializer, serializer_config_key, context, context_key) results[id] = cr end diff --git a/lib/jsonapi/record_accessor.rb b/lib/jsonapi/record_accessor.rb index 7d4e9b6ae..4ab847839 100644 --- a/lib/jsonapi/record_accessor.rb +++ b/lib/jsonapi/record_accessor.rb @@ -11,98 +11,419 @@ def initialize(resource_klass) @_resource_klass = resource_klass end - def model_base_class - # :nocov: - raise 'Abstract method called' - # :nocov: + class << self + def transaction + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def rollback_transaction + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + # Should return an enumerable with the key being the attribute name and value being an array of error messages. + def model_error_messages(model) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def valid?(model, validation_context) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + # Must save without raising an error as well. + # +options+ can include :validate and :raise_on_failure as options. + def save(model, options={}) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def destroy(model, options={}) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def delete_relationship(model, relationship_name, id) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def reload(model) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def model_base_class + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def delete_restriction_error_class + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def record_not_found_error_class + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def find_in_association(model, association_name, ids) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def add_to_association(model, association_name, association_model) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def association_model_class_name(from_model, relationship_name) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + + def set_primary_keys(model, relationship, value) + # :nocov: + raise 'Abstract method called' + # :nocov: + end + end - def delete_restriction_error_class - # :nocov: - raise 'Abstract method called' - # :nocov: + def find_records(filters, options = {}) + if defined?(_resource_klass.find_records) + ActiveSupport::Deprecation.warn "In #{_resource_klass.name} you overrode `find_records`. "\ + "`find_records` has been deprecated in favor of using `apply` "\ + "and `verify` callables on the filter." + + _resource_klass.find_records(filters, options) + else + context = options[:context] + + records = filter_records(filters, options) + + sort_criteria = options.fetch(:sort_criteria) { [] } + order_options = _resource_klass.construct_order_options(sort_criteria) + records = sort_records(records, order_options, context) + + records = apply_pagination(records, options[:paginator], order_options) + + records + end end - def record_not_found_error_class - # :nocov: - raise 'Abstract method called' - # :nocov: + def filter_records(filters, options, records = records(options)) + records = apply_filters(records, filters, options) + apply_includes(records, options) end - def transaction - # :nocov: - raise 'Abstract method called' - # :nocov: + def apply_includes(records, options = {}) + # TODO: See if we can delete these keys out of options since they should really only be for #apply_includes + include_as_join = options[:include_as_join] + include_directives = options[:include_directives] + if include_directives + model_includes = resolve_relationship_names_to_relations(_resource_klass, include_directives.model_includes, options) + records = records_with_includes(records, model_includes, include_as_join) + end + records end - # Should return an enumerable with the key being the attribute name and value being an array of error messages. - def model_error_messages(model) - # :nocov: - raise 'Abstract method called' - # :nocov: + def sort_records(records, order_options, context = {}) + apply_sort(records, order_options, context) end - def rollback_transaction - # :nocov: - raise 'Abstract method called' - # :nocov: + # Converts a chainable relation to an actual Array of records. + # Overwrite if subclass ORM has different implementation. + def get_all(records) + records.all end - # Resource records - def find_resource(_filters, _options = {}) - # :nocov: - raise 'Abstract method called' - # :nocov: + # Overwrite if subclass ORM has different implementation. + def count_records(records) + records.count end - def find_resource_by_key(_key, options = {}) - # :nocov: - raise 'Abstract method called' - # :nocov: + # Eager load the has of includes onto the record and return a chainable relation. + # Overwrite if subclass ORM has different implementation. + # + # +include_as_join+ signifies that the includes should create a join table. Some ORMs will do includes as seperate + # foreign key lookups to avoid huge cascading joins. + def records_with_includes(records, includes, include_as_join=false) + records.includes(includes) end - def find_resources_by_keys(_keys, options = {}) - # :nocov: - raise 'Abstract method called' - # :nocov: + # Overwrite if subclass ORM has different implementation. + def association_relation(model, relation_name) + model.public_send(relation_name) end - def find_count(_filters, _options = {}) + # Returns a chainable relation from the model class. + def model_class_relation # :nocov: raise 'Abstract method called' # :nocov: end - # Relationship records - def related_resource(_resource, _relationship_name, _options = {}) - # :nocov: - raise 'Abstract method called' - # :nocov: + # Implement self.records on the resource if you want to customize the relation for + # finder methods (find, find_by_key, find_serialized_with_caching) + def records(_options = {}) + if defined?(_resource_klass.records) + _resource_klass.records(_options) + else + model_class_relation + end end - def related_resources(_resource, _relationship_name, _options = {}) - # :nocov: - raise 'Abstract method called' - # :nocov: + def apply_pagination(records, paginator, order_options) + records = paginator.apply(records, order_options) if paginator + records end - def count_for_relationship(_resource, _relationship_name, _options = {}) - # :nocov: - raise 'Abstract method called' - # :nocov: + def find_by_key_serialized_with_caching(key, serializer, options = {}) + if _resource_klass.model_class_compatible_with_record_accessor? + results = find_serialized_with_caching({ _resource_klass._primary_key => key }, serializer, options) + result = results.first + fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil? + return result + else + # :nocov: + warn('Caching enabled on model that does not support ActiveRelation') + # :nocov: + end + end + + def find_resource(filters, options = {}) + if options[:caching] && options[:caching][:cache_serializer_output] + find_serialized_with_caching(filters, options[:caching][:serializer], options) + else + resources_for(find_records(filters, options), options[:context]) + end + end + + # Gets all the given resources for a +records+ relation and calls #get_all to ensure all the records are + # queried and returned to the Resource.resources_for function. + def resources_for(records, context) + _resource_klass.resources_for(get_all(records), context) + end + + def find_resources_by_keys(keys, options = {}) + records = records(options) + records = apply_includes(records, options) + records = records.where({ _resource_klass._primary_key => keys }) + + resources_for(records, options[:context]) + end + + def find_resource_by_key(key, options = {}) + if options[:caching] && options[:caching][:cache_serializer_output] + find_by_key_serialized_with_caching(key, options[:caching][:serializer], options) + else + records = find_records({ _resource_klass._primary_key => key }, options.except(:paginator, :sort_criteria)) + model = records.first + fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil? + _resource_klass.resource_for(model, options[:context]) + end + end + + def find_count(filters, options = {}) + count_records(filter_records(filters, options)) + end + + def count_for_relationship(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + context = resource.context + + relation_name = relationship.relation_name(context: context) + records = records_for(resource, relation_name) + + resource_klass = relationship.resource_klass + + filters = options.fetch(:filters, {}) + unless filters.nil? || filters.empty? + records = resource_klass._record_accessor.apply_filters(records, filters, options) + end + + count_records(records) + end + + def records_for_relationship(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + context = resource.context + + relation_name = relationship.relation_name(context: context) + records = records_for(resource, relation_name) + + resource_klass = relationship.resource_klass + + filters = options.fetch(:filters, {}) + unless filters.nil? || filters.empty? + records = resource_klass._record_accessor.apply_filters(records, filters, options) + end + + sort_criteria = options.fetch(:sort_criteria, {}) + order_options = relationship.resource_klass.construct_order_options(sort_criteria) + records = apply_sort(records, order_options, context) + + paginator = options[:paginator] + if paginator + records = apply_pagination(records, paginator, order_options) + end + + records + end + + def foreign_key(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + if relationship.belongs_to? + resource._model.method(relationship.foreign_key).call + else + records = records_for_relationship(resource, relationship_name, options) + return nil if records.nil? + records.public_send(relationship.resource_klass._primary_key) + end + end + + def foreign_keys(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + records = records_for_relationship(resource, relationship_name, options) + records.collect do |record| + record.public_send(relationship.resource_klass._primary_key) + end + end + + def related_resource(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + + if relationship.polymorphic? + associated_model = records_for_relationship(resource, relationship_name, options) + resource_klass = resource.class.resource_klass_for_model(associated_model) if associated_model + return resource_klass.new(associated_model, resource.context) if resource_klass && associated_model + else + resource_klass = relationship.resource_klass + if resource_klass + associated_model = records_for_relationship(resource, relationship_name, options) + return associated_model ? resource_klass.new(associated_model, resource.context) : nil + end + end + end + + def related_resources(resource, relationship_name, options = {}) + relationship = resource.class._relationships[relationship_name.to_sym] + relationship_resource_klass = relationship.resource_klass + + if options[:caching] && options[:caching][:cache_serializer_output] + scope = relationship_resource_klass._record_accessor.records_for_relationship(resource, relationship_name, options) + relationship_resource_klass._record_accessor.find_serialized_with_caching(scope, options[:caching][:serializer], options) + else + records = records_for_relationship(resource, relationship_name, options) + return records.collect do |record| + klass = relationship.polymorphic? ? resource.class.resource_klass_for_model(record) : relationship_resource_klass + klass.new(record, resource.context) + end + end end - # Keys - def foreign_key(_resource, _relationship_name, options = {}) + def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {}) + case model_includes + when Array + return model_includes.map do |value| + resolve_relationship_names_to_relations(resource_klass, value, options) + end + 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) + end + return model_includes + when Symbol + relationship = resource_klass._relationships[model_includes] + return relationship.relation_name(options) + end + end + + # Implement records_for on the resource to customize how the associated records + # are fetched for a model. Particularly helpful for authorization. + def records_for(resource, relation_name) + if resource.respond_to?(:records_for) + return resource.records_for(relation_name) + end + + relationship = resource.class._relationships[relation_name] + + if relationship.is_a?(JSONAPI::Relationship::ToMany) + if resource.respond_to?(:"records_for_#{relation_name}") + return resource.method(:"records_for_#{relation_name}").call + end + else + if resource.respond_to?(:"record_for_#{relation_name}") + return resource.method(:"record_for_#{relation_name}").call + end + end + + association_relation(resource._model, relation_name) + end + + def apply_filters(records, filters, options = {}) + to_many_filters = [] + + if filters + filters.each do |filter, value| + if _resource_klass._relationships.include?(filter) + if _resource_klass._relationships[filter].belongs_to? + records = apply_filter(records, _resource_klass._relationships[filter].foreign_key, value, options) + else + to_many_filters << [filter, value] + end + else + records = apply_filter(records, filter, value, options) + end + end + end + + if to_many_filters.any? + records = apply_filters_to_many_relationships(records, to_many_filters, options) + end + + records + end + + # Apply an array of "to many" relationships to a set of records. + # + # Returns a collection of +records+. + def apply_filters_to_many_relationships(records, to_many_filters, options) # :nocov: raise 'Abstract method called' # :nocov: end - def foreign_keys(_resource, _relationship_name, _options = {}) + def pluck_attributes(relation, model_class, *attrs) # :nocov: raise 'Abstract method called' # :nocov: end + end end \ No newline at end of file diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 0b2a1d487..e49022b53 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -117,9 +117,8 @@ def fetchable_fields self.class.fields end - # Some or def model_error_messages - self.class._record_accessor.model_error_messages(_model) + self.class._record_accessor_klass.model_error_messages(_model) end # Add metadata to validation error objects. @@ -194,12 +193,12 @@ def save # end # ``` def _save(validation_context = nil) - unless @model.valid?(validation_context) + unless self.class._record_accessor_klass.valid?(@model, validation_context) fail JSONAPI::Exceptions::ValidationErrors.new(self) end - if defined? @model.save - saved = @model.save(validate: false) + if defined?(@model.save) + saved = self.class._record_accessor_klass.save(@model, validate: false, raise_on_failure: false) unless saved if @model.errors.present? @@ -211,7 +210,7 @@ def _save(validation_context = nil) else saved = true end - @model.reload if @reload_needed + self.class._record_accessor_klass.reload(@model) if @reload_needed @reload_needed = false @save_needed = !saved @@ -220,12 +219,12 @@ def _save(validation_context = nil) end def _remove - unless @model.destroy + unless self.class._record_accessor_klass.destroy(@model, raise_on_failure: false) fail JSONAPI::Exceptions::ValidationErrors.new(self) end :completed - rescue self.class._record_accessor.delete_restriction_error_class => e + rescue self.class._record_accessor_klass.delete_restriction_error_class => e fail JSONAPI::Exceptions::RecordLocked.new(e.message) end @@ -246,14 +245,14 @@ def _create_to_many_links(relationship_type, relationship_key_values, options) # check if relationship_key_values are already members of this relationship relation_name = relationship.relation_name(context: @context) - existing_relations = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_values) + existing_relations = self.class._record_accessor_klass.find_in_association(@model, relation_name, relationship_key_values) if existing_relations.count > 0 # todo: obscure id so not to leak info fail JSONAPI::Exceptions::HasManyRelationExists.new(existing_relations.first.id) end if options[:reflected_source] - @model.public_send(relation_name) << options[:reflected_source]._model + self.class._record_accessor_klass.add_to_association(@model, relation_name, options[:reflected_source]._model) return :completed end @@ -278,7 +277,7 @@ def _create_to_many_links(relationship_type, relationship_key_values, options) end @reload_needed = true else - @model.public_send(relation_name) << related_resource._model + self.class._record_accessor_klass.add_to_association(@model, relation_name, related_resource._model) end end @@ -350,14 +349,13 @@ def _remove_to_many_link(relationship_type, key, options) @reload_needed = true else - @model.public_send(relationship.relation_name(context: @context)).delete(key) + self.class._record_accessor_klass.delete_relationship(@model, relationship.relation_name(context: @context).to_s, key) end :completed - - rescue self.class._record_accessor.delete_restriction_error_class => e + rescue self.class._record_accessor_klass.delete_restriction_error_class => e fail JSONAPI::Exceptions::RecordLocked.new(e.message) - rescue self.class._record_accessor.record_not_found_error_class + rescue self.class._record_accessor_klass.record_not_found_error_class fail JSONAPI::Exceptions::RecordNotFound.new(key) end @@ -518,13 +516,19 @@ def attribute(attr, options = {}) @_attributes ||= {} @_attributes[attr] = options - define_method attr do - @model.public_send(options[:delegate] ? options[:delegate].to_sym : attr) - end unless method_defined?(attr) - define_method "#{attr}=" do |value| - @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value) - end unless method_defined?("#{attr}=") + delegate = lambda do |model| + if options[:delegate] + options[:delegate].to_sym + elsif attr.to_sym == :id + model.class.primary_key + else + attr + end + end + + define_method(attr) { @model.public_send(delegate[@model]) } unless method_defined?(attr) + define_method("#{attr}=") { |value| @model.public_send("#{delegate[@model]}=", value) } unless method_defined?("#{attr}=") end def default_attribute_options @@ -810,7 +814,7 @@ def paginator(paginator) end def _record_accessor - @_record_accessor = _record_accessor_klass.new(self) + @_record_accessor ||= _record_accessor_klass.new(self) end def record_accessor=(record_accessor_klass) @@ -873,6 +877,10 @@ def _model_class @model_class end + def model_class_compatible_with_record_accessor? + _model_class && _model_class < _record_accessor_klass.model_base_class + end + def _allowed_filter?(filter) !_allowed_filters[filter].nil? end @@ -915,8 +923,8 @@ def _add_relationship(klass, *attrs) # ResourceBuilder methods def define_relationship_methods(relationship_name, relationship_klass, options) # Initialize from an ORM model's properties - if _model_class && _model_class < _record_accessor.model_base_class && - (association_model_class_name = _record_accessor.association_model_class_name(_model_class, relationship_name)) + if model_class_compatible_with_record_accessor? && + (association_model_class_name = _record_accessor_klass.association_model_class_name(_model_class, relationship_name)) options = options.reverse_merge(class_name: association_model_class_name) end @@ -942,7 +950,7 @@ def define_relationship_methods(relationship_name, relationship_klass, options) def define_foreign_key_setter(relationship) define_on_resource "#{relationship.foreign_key}=" do |value| - _model.method("#{relationship.foreign_key}=").call(value) + self.class._record_accessor_klass.set_primary_keys(_model, relationship, value) end end diff --git a/lib/jsonapi/sequel_record_accessor.rb b/lib/jsonapi/sequel_record_accessor.rb index bc60085a6..645e8255f 100644 --- a/lib/jsonapi/sequel_record_accessor.rb +++ b/lib/jsonapi/sequel_record_accessor.rb @@ -1,241 +1,129 @@ require 'jsonapi/record_accessor' +require 'sequel/plugins/association_pks' module JSONAPI class SequelRecordAccessor < RecordAccessor - def transaction - ::Sequel.transaction(::Sequel::DATABASES) do - yield + class << self + def transaction + ::Sequel.transaction(::Sequel::DATABASES) do + yield + end end - end - - def rollback_transaction - fail ::Sequel::Rollback - end - - def model_error_messages(model) - model.errors - end - - def model_base_class - Sequel::Model - end - def delete_restriction_error_class - ActiveRecord::DeleteRestrictionError - end - - def record_not_found_error_class - ActiveRecord::RecordNotFound - end - - def association_model_class_name(from_model, relationship_name) - (reflect = from_model.association_reflections[relationship_name]) && - reflect[:class_name] && reflect[:class_name].gsub(/^::/, '') # Sequel puts "::" in the beginning - end - - def find_resource(filters, options = {}) - if options[:caching] && options[:caching][:cache_serializer_output] - find_serialized_with_caching(filters, options[:caching][:serializer], options) - else - _resource_klass.resources_for(find_records(filters, options), options[:context]) + def rollback_transaction + fail ::Sequel::Rollback end - end - def find_resource_by_key(key, options = {}) - if options[:caching] && options[:caching][:cache_serializer_output] - find_by_key_serialized_with_caching(key, options[:caching][:serializer], options) - else - records = find_records({ _resource_klass._primary_key => key }, options.except(:paginator, :sort_criteria)) - model = records.first - fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil? - _resource_klass.resource_for(model, options[:context]) + def model_error_messages(model) + model.errors end - end - - def find_resources_by_keys(keys, options = {}) - records = records(options) - records = apply_includes(records, options) - records = records.where({ _resource_klass._primary_key => keys }) - - _resource_klass.resources_for(records, options[:context]) - end - - def find_count(filters, options = {}) - count_records(filter_records(filters, options)) - end - def related_resource(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - - if relationship.polymorphic? - associated_model = records_for_relationship(resource, relationship_name, options) - resource_klass = resource.class.resource_klass_for_model(associated_model) if associated_model - return resource_klass.new(associated_model, resource.context) if resource_klass && associated_model - else - resource_klass = relationship.resource_klass - if resource_klass - associated_model = records_for_relationship(resource, relationship_name, options) - return associated_model ? resource_klass.new(associated_model, resource.context) : nil - end + def valid?(model, validation_context) + raise("Sequel does not support validation contexts") if validation_context + model.valid? end - end - def related_resources(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - relationship_resource_klass = relationship.resource_klass - - if options[:caching] && options[:caching][:cache_serializer_output] - scope = relationship_resource_klass._record_accessor.records_for_relationship(resource, relationship_name, options) - relationship_resource_klass._record_accessor.find_serialized_with_caching(scope, options[:caching][:serializer], options) - else - records = records_for_relationship(resource, relationship_name, options) - return records.collect do |record| - klass = relationship.polymorphic? ? resource.class.resource_klass_for_model(record) : relationship_resource_klass - klass.new(record, resource.context) - end + def save(model, options={}) + model.save(options) end - end - - def count_for_relationship(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - - context = resource.context - - relation_name = relationship.relation_name(context: context) - records = records_for(resource, relation_name) - resource_klass = relationship.resource_klass - - filters = options.fetch(:filters, {}) - unless filters.nil? || filters.empty? - records = resource_klass._record_accessor.apply_filters(records, filters, options) + def destroy(model, options={}) + model.destroy(options.slice(:raise_on_failure)) end - records.count(:all) - end - - def foreign_key(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - - if relationship.belongs_to? - resource._model.method(relationship.foreign_key).call - else - records = records_for_relationship(resource, relationship_name, options) - return nil if records.nil? - records.public_send(relationship.resource_klass._primary_key) + def delete_relationship(model, relationship_name, id) + model.public_send("remove_#{relationship_name.singularize}", id) end - end - def foreign_keys(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - - records = records_for_relationship(resource, relationship_name, options) - records.collect do |record| - record.public_send(relationship.resource_klass._primary_key) + def reload(model) + model.reload end - end - # protected-ish methods left public for tests and what not - - def find_serialized_with_caching(filters_or_source, serializer, options = {}) - if filters_or_source.is_a?(ActiveRecord::Relation) - return cached_resources_for(filters_or_source, serializer, options) - elsif _resource_klass._model_class.respond_to?(:all) && _resource_klass._model_class.respond_to?(:arel_table) - records = find_records(filters_or_source, options.except(:include_directives)) - return cached_resources_for(records, serializer, options) - else - # :nocov: - warn('Caching enabled on model that does not support ActiveRelation') - # :nocov: + def model_base_class + Sequel::Model end - end - def find_by_key_serialized_with_caching(key, serializer, options = {}) - if _resource_klass._model_class.respond_to?(:all) && _resource_klass._model_class.respond_to?(:arel_table) - results = find_serialized_with_caching({ _resource_klass._primary_key => key }, serializer, options) - result = results.first - fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil? - return result - else - # :nocov: - warn('Caching enabled on model that does not support ActiveRelation') - # :nocov: + def delete_restriction_error_class + Class.new(Exception) end - end - - def records_for_relationship(resource, relationship_name, options = {}) - relationship = resource.class._relationships[relationship_name.to_sym] - context = resource.context + def record_not_found_error_class + Class.new(Exception) + end - relation_name = relationship.relation_name(context: context) - records = records_for(resource, relation_name) + def find_in_association(model, association_name, ids) + klass = model.class.association_reflection(association_name).associated_class + model.send("#{association_name}_dataset").where(:"#{klass.table_name}__#{klass.primary_key}" => ids) + end - resource_klass = relationship.resource_klass + def add_to_association(model, association_name, association_model) + model.public_send("add_#{association_name.to_s.singularize}", association_model) + end - filters = options.fetch(:filters, {}) - unless filters.nil? || filters.empty? - records = resource_klass._record_accessor.apply_filters(records, filters, options) + def association_model_class_name(from_model, association_name) + (reflect = from_model.association_reflection(association_name)) && + reflect[:class_name] && reflect[:class_name].gsub(/^::/, '') # Sequel puts "::" in the beginning end - sort_criteria = options.fetch(:sort_criteria, {}) - order_options = relationship.resource_klass.construct_order_options(sort_criteria) - records = apply_sort(records, order_options, context) + def set_primary_keys(model, relationship, value) + unless model.class.plugins.include?(Sequel::Plugins::AssociationPks) + raise("Please include the Sequel::Plugins::AssociationPks plugin into the #{model.class} model.") + end + + setter_method = relationship.is_a?(Relationship::ToMany) ? + "#{relationship.name.to_s.singularize}_pks=" : + "#{relationship.foreign_key}=" - paginator = options[:paginator] - if paginator - records = apply_pagination(records, paginator, order_options) + model.public_send(setter_method, value) end - records end - # Implement self.records on the resource if you want to customize the relation for - # finder methods (find, find_by_key, find_serialized_with_caching) - def records(_options = {}) - if defined?(_resource_klass.records) - _resource_klass.records(_options) - else - _resource_klass._model_class.all - end + def model_class_relation + _resource_klass._model_class.dataset end - # Implement records_for on the resource to customize how the associated records - # are fetched for a model. Particularly helpful for authorization. - def records_for(resource, relation_name) - if resource.respond_to?(:records_for) - return resource.records_for(relation_name) - end - - relationship = resource.class._relationships[relation_name] + # protected-ish methods left public for tests and what not - if relationship.is_a?(JSONAPI::Relationship::ToMany) - if resource.respond_to?(:"records_for_#{relation_name}") - return resource.method(:"records_for_#{relation_name}").call - end + def find_serialized_with_caching(filters_or_source, serializer, options = {}) + if filters_or_source.is_a?(Sequel::SQLite::Dataset) + cached_resources_for(filters_or_source, serializer, options) else - if resource.respond_to?(:"record_for_#{relation_name}") - return resource.method(:"record_for_#{relation_name}").call - end + records = find_records(filters_or_source, options.except(:include_directives)) + cached_resources_for(records, serializer, options) end - - resource._model.public_send(relation_name) end - def apply_includes(records, options = {}) - include_directives = options[:include_directives] - if include_directives - model_includes = resolve_relationship_names_to_relations(_resource_klass, include_directives.model_includes, options) - records = records.includes(model_includes) - end - - records - end - def apply_pagination(records, paginator, order_options) - records = paginator.apply(records, order_options) if paginator - records + # Implement self.records on the resource if you want to customize the relation for + # finder methods (find, find_by_key, find_serialized_with_caching) + # def records(_options = {}) + # if defined?(_resource_klass.records) + # _resource_klass.records(_options) + # else + # _resource_klass._model_class.all + # end + # end + # + # def association_relation(model, relation_name) + # relationship = _resource_klass._relationships[relation_name] + # method = relationship.is_a?(JSONAPI::Relationship::ToMany) ? "#{relation_name}_dataset" : relation_name + # model.public_send(method) + # end + + def association_relation(model, relation_name) + # Sequel Reflection classes returning a collection end in "ToMany", so match against that. + method = model.class.association_reflections[relation_name].class.to_s =~ /ToMany/ ? + "#{relation_name}_dataset" : relation_name + + # Aryk: Leave off point. + model.public_send(method) + end + + def records_with_includes(records, includes, include_as_join=false) + method = include_as_join ? :eager_graph : :eager + records.public_send(method, includes) end def apply_sort(records, order_options, context = {}) @@ -243,22 +131,31 @@ def apply_sort(records, order_options, context = {}) _resource_klass.apply_sort(records, order_options, context) else if order_options.any? + columns = [] order_options.each_pair do |field, direction| + table_name = extract_model_class(records).table_name if field.to_s.include?(".") - *model_names, column_name = field.split(".") + *association_names, column_name = field.split(".") + association_graph = association_names. + reverse. + inject(nil) {|hash, assoc| hash ? {assoc.to_sym => hash} : assoc.to_sym } + + records = records.select_all(table_name).association_left_join(association_graph) - associations = _lookup_association_chain([records.model.to_s, *model_names]) - joins_query = _build_joins([records.model, *associations]) + # 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. overridden filters - order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}" - records = records.joins(joins_query).order(order_by_query) + # debugger + columns << Sequel.send(direction, :"#{records.opts[:join].last.table_expr.aliaz}__#{column_name}") + # records = records.association_join(joins_query).select_all(table_name) else - records = records.order(field => direction) + # DB[:items].order(Sequel.desc(:name)) # SELECT * FROM items ORDER BY name DESC + columns << Sequel.send(direction, :"#{table_name}__#{field}") end end + records = records.order(*columns) end - records end end @@ -266,11 +163,11 @@ def apply_sort(records, order_options, context = {}) 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 + association = prev.classify.constantize.all_association_reflections.detect do |assoc| + assoc.association_method.to_s.downcase == current.downcase end associations << association - association.class_name + association.associated_class.to_s end associations @@ -296,76 +193,35 @@ def apply_filter(records, filter, value, options = {}) strategy.call(records, value, options) end else - records.where(filter => value) + # Prefix primary key lookups with the table name to avoid conflicts (Sequel does not do this automatically) + prefixed_filter = _resource_klass._primary_key == filter ? :"#{_resource_klass._model_class.table_name}__#{filter}" : filter + records.where(prefixed_filter => value) end end - # Assumes ActiveRecord's counting. Override if you need a different counting method - def count_records(records) - records.count(:all) - end + def apply_filters_to_many_relationships(records, to_many_filters, options) + required_includes = to_many_filters.map { |filter, _| filter.to_s } + records = apply_includes(records, options.merge( + include_as_join: true, + include_directives: IncludeDirectives.new(_resource_klass, required_includes, force_eager_load: true)), + ) - def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {}) - case model_includes - when Array - return model_includes.map do |value| - resolve_relationship_names_to_relations(resource_klass, value, options) - end - 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) - end - return model_includes - when Symbol - relationship = resource_klass._relationships[model_includes] - return relationship.relation_name(options) - end - end - - def apply_filters(records, filters, options = {}) - required_includes = [] - - if filters - filters.each do |filter, value| - if _resource_klass._relationships.include?(filter) - if _resource_klass._relationships[filter].belongs_to? - records = apply_filter(records, _resource_klass._relationships[filter].foreign_key, value, options) - else - required_includes.push(filter.to_s) - records = apply_filter(records, "#{_resource_klass._relationships[filter].table_name}.#{_resource_klass._relationships[filter].primary_key}", value, options) - end - else - records = apply_filter(records, filter, value, options) - end - end - end - - if required_includes.any? - records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(_resource_klass, required_includes, force_eager_load: true))) + to_many_filters.each do |filter, value| + table_name = records.opts[:join].map(&:table_expr).detect { |t| t.expression == filter }.aliaz + relationship = _resource_klass._relationships[filter] + records = apply_filter(records, :"#{table_name}__#{relationship.primary_key}", value, options) end records end - def filter_records(filters, options, records = records(options)) - records = apply_filters(records, filters, options) - apply_includes(records, options) - end - - def sort_records(records, order_options, context = {}) - apply_sort(records, order_options, context) - end - def cached_resources_for(records, serializer, options) if _resource_klass.caching? - t = _resource_klass._model_class.arel_table - cache_ids = pluck_arel_attributes(records, t[_resource_klass._primary_key], t[_resource_klass._cache_field]) + table = _resource_klass._model_class.table_name + cache_ids = pluck_attributes(records, [table, _resource_klass._primary_key], [table, _resource_klass._cache_field]) resources = CachedResourceFragment.fetch_fragments(_resource_klass, serializer, options[:context], cache_ids) else - resources = _resource_klass.resources_for(records, options[:context]).map { |r| [r.id, r] }.to_h + resources = resources_for(records, options[:context]).map { |r| [r.id, r] }.to_h end preload_included_fragments(resources, records, serializer, options) @@ -373,28 +229,6 @@ def cached_resources_for(records, serializer, options) resources.values end - def find_records(filters, options = {}) - if defined?(_resource_klass.find_records) - ActiveSupport::Deprecation.warn "In #{_resource_klass.name} you overrode `find_records`. "\ - "`find_records` has been deprecated in favor of using `apply` "\ - "and `verify` callables on the filter." - - _resource_klass.find_records(filters, options) - else - context = options[:context] - - records = filter_records(filters, options) - - sort_criteria = options.fetch(:sort_criteria) { [] } - order_options = _resource_klass.construct_order_options(sort_criteria) - records = sort_records(records, order_options, context) - - records = apply_pagination(records, options[:paginator], order_options) - - records - end - end - def preload_included_fragments(resources, records, serializer, options) return if resources.empty? res_ids = resources.keys @@ -414,11 +248,11 @@ def preload_included_fragments(resources, records, serializer, options) include_directives.paths.each do |path| # If path is [:posts, :comments, :author], then... pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at] - pluck_attrs << _resource_klass._model_class.arel_table[_resource_klass._primary_key] + pluck_attrs << [_resource_klass._model_class.table_name, _resource_klass._primary_key] - relation = records - .except(:limit, :offset, :order) - .where({ _resource_klass._primary_key => res_ids }) + relation = records.clone + .tap { |dataset| [:limit, :offset, :order, :where, :join].each { |x| dataset.opts.delete(x) }} + .where({ Sequel[_resource_klass._model_class.table_name][_resource_klass._primary_key] => res_ids }) # These are updated as we iterate through the association path; afterwards they will # refer to the final resource on the path, i.e. the actual resource to find in the cache. @@ -428,7 +262,7 @@ def preload_included_fragments(resources, records, serializer, options) relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author table = nil # people assocs_path = [] # [ :posts, :approved_comments, :author ] - ar_hash = nil # { :posts => { :approved_comments => :author } } + joins_hash = nil # { :posts => { :approved_comments => :author } } # For each step on the path, figure out what the actual table name/alias in the join # will be, and include the primary key of that table in our list of fields to select @@ -443,16 +277,23 @@ def preload_included_fragments(resources, records, serializer, options) end assocs_path << relationship.relation_name(options).to_sym # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }} - ar_hash = assocs_path.reverse.reduce { |memo, step| { step => memo } } - # We can't just look up the table name from the resource class, because Arel could - # have used a table alias if the relation includes a self-reference. - join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node| - arel_node.is_a?(Arel::Nodes::InnerJoin) - end - table = join_source.left + joins_hash = assocs_path.reverse.reduce { |memo, step| { step => memo } } + + # Sequel::Model::Associations::ManyToManyAssociationReflection + # join_source = relation.join(ar_hash).arel.source.right.reverse.find do |arel_node| + # arel_node.is_a?(Arel::Nodes::InnerJoin) + # end + # table = join_source.left + parent_klass = klass klass = relationship.resource_klass - pluck_attrs << table[klass._primary_key] + + # We can't just look up the table name from the resource class, because Sequel could + # have used a table alias if the relation includes a self-reference. + table = relation.association_join(joins_hash).opts[:join].last.table_expr.alias + # pluck_attrs << :"#{_resource_klass._model_class}__#{_resource_klass._primary_key}" + # pluck_attrs << table[klass._primary_key] + pluck_attrs << [table, klass._primary_key] end next unless non_polymorphic @@ -469,16 +310,15 @@ def preload_included_fragments(resources, records, serializer, options) prefilling_resources.flatten!(1) end - pluck_attrs << table[klass._cache_field] if klass.caching? - relation = relation.joins(ar_hash) + pluck_attrs << [table, klass._cache_field] if klass.caching? + relation = relation.association_join(joins_hash).select_all(_resource_klass._model_class.table_name) + # debugger if relationship.is_a?(JSONAPI::Relationship::ToMany) - # Rails doesn't include order clauses in `joins`, so we have to add that manually here. - # FIXME Should find a better way to reflect on relationship ordering. :-( - relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders) + relation = relation.order(parent_klass._model_class.association_reflection(assocs_path.last)[:order]) end # [[post id, comment id, author id, author updated_at], ...] - id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs) + id_rows = pluck_attributes(relation, *pluck_attrs) target_resources[klass.name] ||= {} @@ -495,7 +335,8 @@ def preload_included_fragments(resources, records, serializer, options) .map(&:last) .reject { |id| target_resources[klass.name].has_key?(id) } .uniq - found = klass.find({ klass._primary_key => sub_res_ids }, context: options[:context]) + # debugger + found = klass.find({ :"#{klass._model_class.table_name}__#{klass._primary_key}" => sub_res_ids }, context: options[:context]) target_resources[klass.name].merge! found.map { |r| [r.id, r] }.to_h end @@ -515,14 +356,22 @@ def preload_included_fragments(resources, records, serializer, options) end end - def pluck_arel_attributes(relation, *attrs) - conn = relation.connection - quoted_attrs = attrs.map do |attr| - quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name) - quoted_column = conn.quote_column_name(attr.name) - "#{quoted_table}.#{quoted_column}" + def pluck_attributes(relation, *attrs) + # Use Sequel's Symbol table aliasing convention. + relation.select_map(attrs.map { |table, column| :"#{table}__#{column}___#{table}#{column}" }) + end + + private + + def extract_model_class(records) + if records.is_a?(Sequel::Dataset) + records.opts[:model] + elsif records.is_a?(Class) && records < self.class.model_base_class + records + else + raise("Cannot extract table name from #{records.inspect}") end - relation.pluck(*quoted_attrs) end + end end \ No newline at end of file diff --git a/test/benchmark/reflect_create_and_delete_benchmark.rb b/test/benchmark/reflect_create_and_delete_benchmark.rb index 0efd6b181..22f145bad 100644 --- a/test/benchmark/reflect_create_and_delete_benchmark.rb +++ b/test/benchmark/reflect_create_and_delete_benchmark.rb @@ -2,7 +2,7 @@ class ReflectCreateAndDeleteBenchmark < IntegrationBenchmark def setup - $test_user = Person.find(1) + $test_user = find_first(Person, 1) end def create_and_delete_comments @@ -19,7 +19,7 @@ def create_and_delete_comments 'Accept' => JSONAPI::MEDIA_TYPE } assert_response :no_content - post_object = Post.find(15) + post_object = find_first(Post, 15) assert_equal 3, post_object.comments.collect { |comment| comment.id }.length delete '/posts/15/relationships/comments', params: @@ -35,7 +35,7 @@ def create_and_delete_comments 'Accept' => JSONAPI::MEDIA_TYPE } assert_response :no_content - post_object = Post.find(15) + post_object = find_first(Post, 15) assert_equal 0, post_object.comments.collect { |comment| comment.id }.length end diff --git a/test/benchmark/reflect_update_relationships_benchmark.rb b/test/benchmark/reflect_update_relationships_benchmark.rb index 0eb61f171..bff31c286 100644 --- a/test/benchmark/reflect_update_relationships_benchmark.rb +++ b/test/benchmark/reflect_update_relationships_benchmark.rb @@ -2,7 +2,7 @@ class ReflectUpdateRelationshipsBenchmark < IntegrationBenchmark def setup - $test_user = Person.find(1) + $test_user = find_first(Person, 1) end def replace_tags @@ -16,7 +16,7 @@ def replace_tags 'Accept' => JSONAPI::MEDIA_TYPE } assert_response :no_content - post_object = Post.find(15) + post_object = find_first(Post, 15) assert_equal 5, post_object.tags.collect { |tag| tag.id }.length put '/posts/15/relationships/tags', params: @@ -29,7 +29,7 @@ def replace_tags 'Accept' => JSONAPI::MEDIA_TYPE } assert_response :no_content - post_object = Post.find(15) + post_object = find_first(Post, 15) assert_equal 3, post_object.tags.collect { |tag| tag.id }.length end diff --git a/test/benchmark/request_benchmark.rb b/test/benchmark/request_benchmark.rb index d42d3a695..0607e89c7 100644 --- a/test/benchmark/request_benchmark.rb +++ b/test/benchmark/request_benchmark.rb @@ -3,7 +3,7 @@ class RequestBenchmark < IntegrationBenchmark def setup super - $test_user = Person.find(1) + $test_user = find_first(Person, 1) end def bench_large_index_request_uncached diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index 20fa914f6..bc532bce1 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -430,7 +430,7 @@ def test_sorting_by_multiple_fields def create_alphabetically_first_user_and_post author = Person.create(name: "Aardvark", date_joined: Time.now) - author.posts.create(title: "My first post", body: "Hello World") + Post.create(title: "My first post", body: "Hello World", author_id: author.id) end def test_sorting_by_relationship_field @@ -720,8 +720,8 @@ def test_create_with_invalid_data assert_equal "author - can't be blank", json_response['errors'][0]['detail'] assert_equal "/data/attributes/title", json_response['errors'][1]['source']['pointer'] - assert_equal "is too long (maximum is 35 characters)", json_response['errors'][1]['title'] - assert_equal "title - is too long (maximum is 35 characters)", json_response['errors'][1]['detail'] + assert_match /is too long( \(maximum is 35 characters\))?/, json_response['errors'][1]['title'] + assert_match /title - is too long( \(maximum is 35 characters\))?/, json_response['errors'][1]['detail'] assert_nil response.location end @@ -958,7 +958,7 @@ def test_create_with_links_include_and_fields def test_update_with_links set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -989,7 +989,7 @@ def test_update_with_links def test_update_with_internal_server_error set_content_type_header! - post_object = Post.find(3) + post_object = find_first(Post, 3) title = post_object.title put :update, params: @@ -1005,7 +1005,7 @@ def test_update_with_internal_server_error } assert_response 500 - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_equal title, post_object.title end @@ -1013,7 +1013,7 @@ def test_update_with_links_allow_extra_params JSONAPI.configuration.raise_if_parameters_not_allowed = false set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -1110,27 +1110,27 @@ def test_update_remove_links def test_update_relationship_to_one set_content_type_header! - ruby = Section.find_by(name: 'ruby') - post_object = Post.find(4) + ruby = find_first(Section, name: 'ruby') + post_object = find_first(Post, 4) assert_not_equal ruby.id, post_object.section_id put :update_relationship, params: {post_id: 4, relationship: 'section', data: {type: 'sections', id: "#{ruby.id}"}} assert_response :no_content - post_object = Post.find(4) + post_object = find_first(Post, 4) assert_equal ruby.id, post_object.section_id end def test_update_relationship_to_one_nil set_content_type_header! - ruby = Section.find_by(name: 'ruby') - post_object = Post.find(4) + ruby = find_first(Section, name: 'ruby') + post_object = find_first(Post, 4) assert_not_equal ruby.id, post_object.section_id put :update_relationship, params: {post_id: 4, relationship: 'section', data: nil} assert_response :no_content - post_object = Post.find(4) + post_object = find_first(Post, 4) assert_nil post_object.section_id end @@ -1240,10 +1240,10 @@ def test_update_other_to_many_links_data_nil def test_update_relationship_to_one_singular_param_id_nil set_content_type_header! - ruby = Section.find_by(name: 'ruby') - post_object = Post.find(3) + ruby = find_first(Section, name: 'ruby') + post_object = find_first(Post, 3) post_object.section = ruby - post_object.save! + save!(post_object) put :update_relationship, params: {post_id: 3, relationship: 'section', data: {type: 'sections', id: nil}} @@ -1253,10 +1253,10 @@ def test_update_relationship_to_one_singular_param_id_nil def test_update_relationship_to_one_data_nil set_content_type_header! - ruby = Section.find_by(name: 'ruby') - post_object = Post.find(3) + ruby = find_first(Section, name: 'ruby') + post_object = find_first(Post, 3) post_object.section = ruby - post_object.save! + save!(post_object) put :update_relationship, params: {post_id: 3, relationship: 'section', data: nil} @@ -1266,29 +1266,29 @@ def test_update_relationship_to_one_data_nil def test_remove_relationship_to_one set_content_type_header! - ruby = Section.find_by(name: 'ruby') - post_object = Post.find(3) + ruby = find_first(Section, name: 'ruby') + post_object = find_first(Post, 3) post_object.section_id = ruby.id - post_object.save! + save!(post_object) put :destroy_relationship, params: {post_id: 3, relationship: 'section'} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_nil post_object.section_id end def test_update_relationship_to_one_singular_param set_content_type_header! - ruby = Section.find_by(name: 'ruby') - post_object = Post.find(3) + ruby = find_first(Section, name: 'ruby') + post_object = find_first(Post, 3) post_object.section_id = nil - post_object.save! + save!(post_object) put :update_relationship, params: {post_id: 3, relationship: 'section', data: {type: 'sections', id: "#{ruby.id}"}} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_equal ruby.id, post_object.section_id end @@ -1297,19 +1297,19 @@ def test_update_relationship_to_many_join_table_single put :update_relationship, params: {post_id: 3, relationship: 'tags', data: []} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_equal 0, post_object.tags.length put :update_relationship, params: {post_id: 3, relationship: 'tags', data: [{type: 'tags', id: 2}]} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_equal 1, post_object.tags.length put :update_relationship, params: {post_id: 3, relationship: 'tags', data: [{type: 'tags', id: 5}]} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) tags = post_object.tags.collect { |tag| tag.id } assert_equal 1, tags.length assert matches_array? [5], tags @@ -1320,7 +1320,7 @@ def test_update_relationship_to_many put :update_relationship, params: {post_id: 3, relationship: 'tags', data: [{type: 'tags', id: 2}, {type: 'tags', id: 3}]} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_equal 2, post_object.tags.collect { |tag| tag.id }.length assert matches_array? [2, 3], post_object.tags.collect { |tag| tag.id } end @@ -1330,14 +1330,14 @@ def test_create_relationship_to_many_join_table put :update_relationship, params: {post_id: 3, relationship: 'tags', data: [{type: 'tags', id: 2}, {type: 'tags', id: 3}]} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_equal 2, post_object.tags.collect { |tag| tag.id }.length assert matches_array? [2, 3], post_object.tags.collect { |tag| tag.id } post :create_relationship, params: {post_id: 3, relationship: 'tags', data: [{type: 'tags', id: 5}]} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_equal 3, post_object.tags.collect { |tag| tag.id }.length assert matches_array? [2, 3, 5], post_object.tags.collect { |tag| tag.id } end @@ -1345,13 +1345,13 @@ def test_create_relationship_to_many_join_table def test_create_relationship_to_many_join_table_reflect JSONAPI.configuration.use_relationship_reflection = true set_content_type_header! - post_object = Post.find(15) + post_object = find_first(Post, 15) assert_equal 5, post_object.tags.collect { |tag| tag.id }.length put :update_relationship, params: {post_id: 15, relationship: 'tags', data: [{type: 'tags', id: 2}, {type: 'tags', id: 3}, {type: 'tags', id: 4}]} assert_response :no_content - post_object = Post.find(15) + post_object = find_first(Post, 15) assert_equal 3, post_object.tags.collect { |tag| tag.id }.length assert matches_array? [2, 3, 4], post_object.tags.collect { |tag| tag.id } ensure @@ -1393,7 +1393,7 @@ def test_create_relationship_to_many_missing_data def test_create_relationship_to_many_join_table_no_reflection JSONAPI.configuration.use_relationship_reflection = false set_content_type_header! - p = Post.find(4) + p = find_first(Post, 4) assert_equal [], p.tag_ids post :create_relationship, params: {post_id: 4, relationship: 'tags', data: [{type: 'tags', id: 1}, {type: 'tags', id: 2}, {type: 'tags', id: 3}]} @@ -1408,7 +1408,7 @@ def test_create_relationship_to_many_join_table_no_reflection def test_create_relationship_to_many_join_table_reflection JSONAPI.configuration.use_relationship_reflection = true set_content_type_header! - p = Post.find(4) + p = find_first(Post, 4) assert_equal [], p.tag_ids post :create_relationship, params: {post_id: 4, relationship: 'tags', data: [{type: 'tags', id: 1}, {type: 'tags', id: 2}, {type: 'tags', id: 3}]} @@ -1423,7 +1423,7 @@ def test_create_relationship_to_many_join_table_reflection def test_create_relationship_to_many_no_reflection JSONAPI.configuration.use_relationship_reflection = false set_content_type_header! - p = Post.find(4) + p = find_first(Post, 4) assert_equal [], p.comment_ids post :create_relationship, params: {post_id: 4, relationship: 'comments', data: [{type: 'comments', id: 7}, {type: 'comments', id: 8}]} @@ -1438,7 +1438,7 @@ def test_create_relationship_to_many_no_reflection def test_create_relationship_to_many_reflection JSONAPI.configuration.use_relationship_reflection = true set_content_type_header! - p = Post.find(4) + p = find_first(Post, 4) assert_equal [], p.comment_ids post :create_relationship, params: {post_id: 4, relationship: 'comments', data: [{type: 'comments', id: 7}, {type: 'comments', id: 8}]} @@ -1455,7 +1455,7 @@ def test_create_relationship_to_many_join_table_record_exists put :update_relationship, params: {post_id: 3, relationship: 'tags', data: [{type: 'tags', id: 2}, {type: 'tags', id: 3}]} assert_response :no_content - post_object = Post.find(3) + post_object = find_first(Post, 3) assert_equal 2, post_object.tags.collect { |tag| tag.id }.length assert matches_array? [2, 3], post_object.tags.collect { |tag| tag.id } @@ -1487,7 +1487,7 @@ def test_delete_relationship_to_many } assert_response :no_content - p = Post.find(14) + p = find_first(Post, 14) assert_equal [2, 3, 4], p.tag_ids delete :destroy_relationship, @@ -1512,14 +1512,14 @@ def test_delete_relationship_to_many_with_relationship_url_not_matching_type post :create_relationship, params: {post_id: 14, relationship: 'special_tags', data: [{type: 'tags', id: 2}]} #check the relationship was created successfully - assert_equal 1, Post.find(14).special_tags.count - before_tags = Post.find(14).tags.count + assert_equal 1, find_first(Post, 14).special_tags.count + before_tags = find_first(Post, 14).tags.count delete :destroy_relationship, params: {post_id: 14, relationship: 'special_tags', data: [{type: 'tags', id: 2}]} - assert_equal 0, Post.find(14).special_tags.count, "Relationship that matches URL relationship not destroyed" + assert_equal 0, find_first(Post, 14).special_tags.count, "Relationship that matches URL relationship not destroyed" #check that the tag association is not affected - assert_equal Post.find(14).tags.count, before_tags + assert_equal find_first(Post, 14).tags.count, before_tags ensure PostResource.instance_variable_get(:@_relationships).delete(:special_tags) end @@ -1528,7 +1528,7 @@ def test_delete_relationship_to_many_does_not_exist set_content_type_header! put :update_relationship, params: {post_id: 14, relationship: 'tags', data: [{type: 'tags', id: 2}, {type: 'tags', id: 3}]} assert_response :no_content - p = Post.find(14) + p = find_first(Post, 14) assert_equal [2, 3], p.tag_ids delete :destroy_relationship, params: {post_id: 14, relationship: 'tags', data: [{type: 'tags', id: 4}]} @@ -1542,7 +1542,7 @@ def test_delete_relationship_to_many_with_empty_data set_content_type_header! put :update_relationship, params: {post_id: 14, relationship: 'tags', data: [{type: 'tags', id: 2}, {type: 'tags', id: 3}]} assert_response :no_content - p = Post.find(14) + p = find_first(Post, 14) assert_equal [2, 3], p.tag_ids put :update_relationship, params: {post_id: 14, relationship: 'tags', data: [] } @@ -1554,7 +1554,7 @@ def test_delete_relationship_to_many_with_empty_data def test_update_mismatch_single_key set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -1578,7 +1578,7 @@ def test_update_mismatch_single_key def test_update_extra_param set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -1603,7 +1603,7 @@ def test_update_extra_param def test_update_extra_param_in_links set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -1631,7 +1631,6 @@ def test_update_extra_param_in_links_allow_extra_params JSONAPI.configuration.use_text_errors = true set_content_type_header! - javascript = Section.find_by(name: 'javascript') put :update, params: { @@ -1660,7 +1659,7 @@ def test_update_extra_param_in_links_allow_extra_params def test_update_missing_param set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -1701,7 +1700,7 @@ def test_update_missing_key def test_update_missing_type set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -1725,7 +1724,7 @@ def test_update_missing_type def test_update_unknown_key set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -1750,7 +1749,7 @@ def test_update_unknown_key def test_update_multiple_ids set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { id: '3,16', @@ -1774,7 +1773,7 @@ def test_update_multiple_ids def test_update_multiple_array set_content_type_header! - javascript = Section.find_by(name: 'javascript') + javascript = find_first(Section, name: 'javascript') put :update, params: { @@ -1843,7 +1842,8 @@ def test_update_bad_attributes end def test_delete_with_validation_error - post = Post.create!(title: "can't destroy me", author: Person.first) + post = Post.new(title: "can't destroy me", author: Person.first) + save!(post) delete :destroy, params: { id: post.id } assert_equal "can't destroy me", json_response['errors'][0]['title'] @@ -2514,7 +2514,7 @@ def test_get_related_resource_nil class BooksControllerTest < ActionController::TestCase def test_books_include_correct_type - $test_user = Person.find(1) + $test_user = find_first(Person, 1) assert_cacheable_get :index, params: {filter: {id: '1'}, include: 'authors'} assert_response :success assert_equal 'authors', json_response['included'][0]['type'] @@ -2523,23 +2523,22 @@ def test_books_include_correct_type def test_destroy_relationship_has_and_belongs_to_many JSONAPI.configuration.use_relationship_reflection = false - assert_equal 2, Book.find(2).authors.count + assert_equal 2, find_first(Book, 2).authors.count delete :destroy_relationship, params: {book_id: 2, relationship: 'authors', data: [{type: 'authors', id: 1}]} assert_response :no_content - assert_equal 1, Book.find(2).authors.count + assert_equal 1, find_first(Book, 2).authors.count ensure JSONAPI.configuration.use_relationship_reflection = false end def test_destroy_relationship_has_and_belongs_to_many_reflect JSONAPI.configuration.use_relationship_reflection = true - - assert_equal 2, Book.find(2).authors.count + assert_equal 2, find_first(Book, 2).authors.count delete :destroy_relationship, params: {book_id: 2, relationship: 'authors', data: [{type: 'authors', id: 1}]} assert_response :no_content - assert_equal 1, Book.find(2).authors.count + assert_equal 1, find_first(Book, 2).authors.count ensure JSONAPI.configuration.use_relationship_reflection = false @@ -2885,7 +2884,7 @@ def test_create_with_invalid_data class Api::V2::BooksControllerTest < ActionController::TestCase def setup JSONAPI.configuration.json_key_format = :dasherized_key - $test_user = Person.find(1) + $test_user = find_first(Person, 1) end def after_teardown @@ -2979,7 +2978,6 @@ def test_books_offset_pagination_no_params_includes_query_count_two_levels def test_books_offset_pagination Api::V2::BookResource.paginator :offset - assert_cacheable_get :index, params: {page: {offset: 50, limit: 12}} assert_response :success assert_equal 12, json_response['data'].size @@ -3106,7 +3104,7 @@ def test_books_included_paged end def test_books_banned_non_book_admin - $test_user = Person.find(1) + $test_user = find_first(Person, 1) Api::V2::BookResource.paginator :offset JSONAPI.configuration.top_level_meta_include_record_count = true assert_query_count(2) do @@ -3121,7 +3119,7 @@ def test_books_banned_non_book_admin end def test_books_banned_non_book_admin_includes_switched - $test_user = Person.find(1) + $test_user = find_first(Person, 1) Api::V2::BookResource.paginator :offset JSONAPI.configuration.top_level_meta_include_record_count = true assert_query_count(3) do @@ -3140,7 +3138,7 @@ def test_books_banned_non_book_admin_includes_switched end def test_books_banned_non_book_admin_includes_nested_includes - $test_user = Person.find(1) + $test_user = find_first(Person, 1) JSONAPI.configuration.top_level_meta_include_record_count = true Api::V2::BookResource.paginator :offset assert_query_count(4) do @@ -3156,7 +3154,7 @@ def test_books_banned_non_book_admin_includes_nested_includes end def test_books_banned_admin - $test_user = Person.find(5) + $test_user = find_first(Person, 5) Api::V2::BookResource.paginator :offset JSONAPI.configuration.top_level_meta_include_record_count = true assert_query_count(2) do @@ -3171,7 +3169,7 @@ def test_books_banned_admin end def test_books_not_banned_admin - $test_user = Person.find(5) + $test_user = find_first(Person, 5) Api::V2::BookResource.paginator :offset JSONAPI.configuration.top_level_meta_include_record_count = true assert_query_count(2) do @@ -3186,7 +3184,7 @@ def test_books_not_banned_admin end def test_books_banned_non_book_admin_overlapped - $test_user = Person.find(1) + $test_user = find_first(Person, 1) Api::V2::BookResource.paginator :offset JSONAPI.configuration.top_level_meta_include_record_count = true assert_query_count(2) do @@ -3201,7 +3199,7 @@ def test_books_banned_non_book_admin_overlapped end def test_books_included_exclude_unapproved - $test_user = Person.find(1) + $test_user = find_first(Person, 1) Api::V2::BookResource.paginator :none assert_query_count(2) do @@ -3215,7 +3213,7 @@ def test_books_included_exclude_unapproved end def test_books_included_all_comments_for_admin - $test_user = Person.find(5) + $test_user = find_first(Person, 5) Api::V2::BookResource.paginator :none assert_cacheable_get :index, params: {filter: {id: '0,1,2,3,4'}, include: 'book-comments'} @@ -3227,14 +3225,14 @@ def test_books_included_all_comments_for_admin end def test_books_filter_by_book_comment_id_limited_user - $test_user = Person.find(1) + $test_user = find_first(Person, 1) assert_cacheable_get :index, params: {filter: {book_comments: '0,52' }} assert_response :success assert_equal 1, json_response['data'].size end def test_books_filter_by_book_comment_id_admin_user - $test_user = Person.find(5) + $test_user = find_first(Person, 5) assert_cacheable_get :index, params: {filter: {book_comments: '0,52' }} assert_response :success assert_equal 2, json_response['data'].size @@ -3242,7 +3240,7 @@ def test_books_filter_by_book_comment_id_admin_user def test_books_create_unapproved_comment_limited_user_using_relation_name set_content_type_header! - $test_user = Person.find(1) + $test_user = find_first(Person, 1) book_comment = BookComment.create(body: 'Not Approved dummy comment', approved: false) post :create_relationship, params: {book_id: 1, relationship: 'book_comments', data: [{type: 'book_comments', id: book_comment.id}]} @@ -3256,7 +3254,7 @@ def test_books_create_unapproved_comment_limited_user_using_relation_name def test_books_create_approved_comment_limited_user_using_relation_name set_content_type_header! - $test_user = Person.find(1) + $test_user = find_first(Person, 1) book_comment = BookComment.create(body: 'Approved dummy comment', approved: true) post :create_relationship, params: {book_id: 1, relationship: 'book_comments', data: [{type: 'book_comments', id: book_comment.id}]} @@ -3267,7 +3265,7 @@ def test_books_create_approved_comment_limited_user_using_relation_name end def test_books_delete_unapproved_comment_limited_user_using_relation_name - $test_user = Person.find(1) + $test_user = find_first(Person, 1) book_comment = BookComment.create(book_id: 1, body: 'Not Approved dummy comment', approved: false) delete :destroy_relationship, params: {book_id: 1, relationship: 'book_comments', data: [{type: 'book_comments', id: book_comment.id}]} @@ -3278,7 +3276,7 @@ def test_books_delete_unapproved_comment_limited_user_using_relation_name end def test_books_delete_approved_comment_limited_user_using_relation_name - $test_user = Person.find(1) + $test_user = find_first(Person, 1) book_comment = BookComment.create(book_id: 1, body: 'Approved dummy comment', approved: true) delete :destroy_relationship, params: {book_id: 1, relationship: 'book_comments', data: [{type: 'book_comments', id: book_comment.id}]} @@ -3290,7 +3288,7 @@ def test_books_delete_approved_comment_limited_user_using_relation_name def test_books_delete_approved_comment_limited_user_using_relation_name_reflected JSONAPI.configuration.use_relationship_reflection = true - $test_user = Person.find(1) + $test_user = find_first(Person, 1) book_comment = BookComment.create(book_id: 1, body: 'Approved dummy comment', approved: true) delete :destroy_relationship, params: {book_id: 1, relationship: 'book_comments', data: [{type: 'book_comments', id: book_comment.id}]} @@ -3306,11 +3304,11 @@ class Api::V2::BookCommentsControllerTest < ActionController::TestCase def setup JSONAPI.configuration.json_key_format = :dasherized_key Api::V2::BookCommentResource.paginator :none - $test_user = Person.find(1) + $test_user = find_first(Person, 1) end def test_book_comments_all_for_admin - $test_user = Person.find(5) + $test_user = find_first(Person, 5) assert_query_count(1) do assert_cacheable_get :index end @@ -3319,7 +3317,7 @@ def test_book_comments_all_for_admin end def test_book_comments_unapproved_context_based - $test_user = Person.find(5) + $test_user = find_first(Person, 5) assert_query_count(1) do assert_cacheable_get :index, params: {filter: {approved: 'false'}} end @@ -3328,7 +3326,7 @@ def test_book_comments_unapproved_context_based end def test_book_comments_exclude_unapproved_context_based - $test_user = Person.find(1) + $test_user = find_first(Person, 1) assert_query_count(1) do assert_cacheable_get :index end @@ -3485,7 +3483,7 @@ def test_get_related_resources end def test_get_related_resources_filtered - $test_user = Person.find(1) + $test_user = find_first(Person, 1) get :get_related_resources, params: {moon_id: '1', relationship: 'craters', source: "api/v1/moons", filter: {description: 'Small crater'}} assert_response :success assert_hash_equals({ @@ -3636,6 +3634,7 @@ def test_whitelisted_error_in_controller end end +# These specs have ORM-specific implementations, please see support//app_config.rb class Api::V6::PostsControllerTest < ActionController::TestCase def test_caching_with_join_from_resource_with_sql_fragment assert_cacheable_get :index, params: {include: 'section'} diff --git a/test/helpers/configuration_helpers.rb b/test/helpers/configuration_helpers.rb index ed7169700..ef7b2eb78 100644 --- a/test/helpers/configuration_helpers.rb +++ b/test/helpers/configuration_helpers.rb @@ -30,9 +30,8 @@ def with_resource_caching(cache, classes = :all) resource_classes = ObjectSpace.each_object(Class).select do |klass| if klass < JSONAPI::Resource # Not using Resource#_model_class to avoid tripping the warning early, which could - # cause ResourceTest#test_nil_model_class to fail. - model_class = klass._model_name.to_s.safe_constantize - if model_class && model_class.respond_to?(:arel_table) + # cause ResourceTest#test_nil_model_class to fail, so we check first with safe_constantize. + if klass._model_name.to_s.safe_constantize && klass.model_class_compatible_with_record_accessor? next true end end diff --git a/test/helpers/record_accessor_helpers.rb b/test/helpers/record_accessor_helpers.rb new file mode 100644 index 000000000..c9ee5100c --- /dev/null +++ b/test/helpers/record_accessor_helpers.rb @@ -0,0 +1,21 @@ +module Helpers +# Test specific methods needed by each ORM for test cases to work. + module RecordAccessorHelpers + def find_first(model_class, id) + find_all(model_class, id).first + end + + # Written to be ORM agnostic so long as the orm implements a #where method which responds to #all + # or #first, which many of them do. If needed this, can be abstracted out into the RecordAccessor, but + # since they are only used for tests, I didn't want to add test-only logic into the library. + def find_all(model_class, *ids) + model_class.where(ids.first.is_a?(Hash) ? ids.first : {id: ids}) + end + + def save!(model) + JSONAPI.configuration.default_record_accessor_klass.save(model, raise_on_failure: true) + end + + end + +end \ No newline at end of file diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index 8a27de962..cc98723aa 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -5,7 +5,7 @@ def setup JSONAPI.configuration.json_key_format = :underscored_key JSONAPI.configuration.route_format = :underscored_route Api::V2::BookResource.paginator :offset - $test_user = Person.find(1) + $test_user = find_first(Person, 1) end def after_teardown @@ -259,14 +259,14 @@ def test_post_single_minimal_invalid end def test_update_relationship_without_content_type - ruby = Section.find_by(name: 'ruby') + ruby = find_first(Section, name: 'ruby') patch '/posts/3/relationships/section', params: { 'data' => {'type' => 'sections', 'id' => ruby.id.to_s }}.to_json assert_equal 415, status end def test_patch_update_relationship_to_one - ruby = Section.find_by(name: 'ruby') + ruby = find_first(Section, name: 'ruby') patch '/posts/3/relationships/section', params: { 'data' => {'type' => 'sections', 'id' => ruby.id.to_s }}.to_json, headers: { @@ -278,7 +278,7 @@ def test_patch_update_relationship_to_one end def test_put_update_relationship_to_one - ruby = Section.find_by(name: 'ruby') + ruby = find_first(Section, name: 'ruby') put '/posts/3/relationships/section', params: { 'data' => {'type' => 'sections', 'id' => ruby.id.to_s }}.to_json, headers: { 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE, @@ -291,7 +291,7 @@ def test_put_update_relationship_to_one def test_patch_update_relationship_to_many_acts_as_set # Comments are acts_as_set=false so PUT/PATCH should respond with 403 - rogue = Comment.find_by(body: 'Rogue Comment Here') + rogue = find_first(Comment, body: 'Rogue Comment Here') patch '/posts/5/relationships/comments', params: { 'data' => [{'type' => 'comments', 'id' => rogue.id.to_s }]}.to_json, headers: { 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE, @@ -302,7 +302,7 @@ def test_patch_update_relationship_to_many_acts_as_set end def test_post_update_relationship_to_many - rogue = Comment.find_by(body: 'Rogue Comment Here') + rogue = find_first(Comment, body: 'Rogue Comment Here') post '/posts/5/relationships/comments', params: { 'data' => [{'type' => 'comments', 'id' => rogue.id.to_s }]}.to_json, headers: { 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE, @@ -315,7 +315,7 @@ def test_post_update_relationship_to_many def test_put_update_relationship_to_many_acts_as_set # Comments are acts_as_set=false so PUT/PATCH should respond with 403. Note: JR currently treats PUT and PATCH as equivalent - rogue = Comment.find_by(body: 'Rogue Comment Here') + rogue = find_first(Comment, body: 'Rogue Comment Here') put '/posts/5/relationships/comments', params: { 'data' => [{'type' => 'comments', 'id' => rogue.id.to_s }]}.to_json, headers: { 'CONTENT_TYPE' => JSONAPI::MEDIA_TYPE, @@ -896,7 +896,7 @@ def test_patch_formatted_dasherized_replace_to_many def test_patch_formatted_dasherized_replace_to_many_computed_relation $original_test_user = $test_user - $test_user = Person.find(5) + $test_user = find_first(Person, 5) original_config = JSONAPI.configuration.dup JSONAPI.configuration.route_format = :dasherized_route JSONAPI.configuration.json_key_format = :dasherized_key @@ -955,7 +955,7 @@ def test_post_to_many_link def test_post_computed_relation_to_many $original_test_user = $test_user - $test_user = Person.find(5) + $test_user = find_first(Person, 5) original_config = JSONAPI.configuration.dup JSONAPI.configuration.route_format = :dasherized_route JSONAPI.configuration.json_key_format = :dasherized_key @@ -1000,7 +1000,7 @@ def test_patch_to_many_link def test_patch_to_many_link_computed_relation $original_test_user = $test_user - $test_user = Person.find(5) + $test_user = find_first(Person, 5) original_config = JSONAPI.configuration.dup JSONAPI.configuration.route_format = :dasherized_route JSONAPI.configuration.json_key_format = :dasherized_key diff --git a/test/support/sequel/setup.rb b/test/support/active_record/app_config.rb similarity index 100% rename from test/support/sequel/setup.rb rename to test/support/active_record/app_config.rb diff --git a/test/support/active_record/import_schema.rb b/test/support/active_record/import_schema.rb new file mode 100644 index 000000000..d33fb244e --- /dev/null +++ b/test/support/active_record/import_schema.rb @@ -0,0 +1,11 @@ +require 'active_record' + +connection = ActiveRecord::Base.connection + +sql = File.read(File.expand_path('../../database/dump.sql', __FILE__)) +statements = sql.split(/;$/) +statements.pop # the last empty statement + +statements.each do |statement| + connection.execute(statement) +end \ No newline at end of file diff --git a/test/support/active_record/models.rb b/test/support/active_record/models.rb index 021ecae83..577bdaf58 100644 --- a/test/support/active_record/models.rb +++ b/test/support/active_record/models.rb @@ -1,10 +1,3 @@ -require_relative 'schema' - -ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.uncountable 'preferences' - inflect.irregular 'numero_telefone', 'numeros_telefone' -end - class Person < ActiveRecord::Base has_many :posts, foreign_key: 'author_id' has_many :comments, foreign_key: 'author_id' @@ -58,6 +51,31 @@ def destroy_callback end end +class PostWithBadAfterSave < ActiveRecord::Base + self.table_name = 'posts' + after_save :do_some_after_save_stuff + + def do_some_after_save_stuff + errors[:base] << 'Boom! Error added in after_save callback.' + raise ActiveRecord::RecordInvalid.new(self) + end +end + +class PostWithCustomValidationContext < ActiveRecord::Base + self.table_name = 'posts' + validate :api_specific_check, on: :json_api_create + + def api_specific_check + errors[:base] << 'Record is invalid' + end +end + +module Legacy + class FlatPost < ActiveRecord::Base + self.table_name = "posts" + end +end + class SpecialPostTag < ActiveRecord::Base belongs_to :tag belongs_to :post diff --git a/test/support/active_record/rollback.rb b/test/support/active_record/rollback.rb new file mode 100644 index 000000000..b386ad42b --- /dev/null +++ b/test/support/active_record/rollback.rb @@ -0,0 +1,21 @@ +module Minitest + module Rollback + + def before_setup + ActiveRecord::Base.connection.begin_transaction joinable: false + super + end + + def after_teardown + super + conn = ActiveRecord::Base.connection + conn.rollback_transaction if conn.transaction_open? + ActiveRecord::Base.clear_active_connections! + end + + end + + class Test + include Rollback + end +end \ No newline at end of file diff --git a/test/support/active_record/schema.rb b/test/support/active_record/schema.rb deleted file mode 100644 index 2b76e9b11..000000000 --- a/test/support/active_record/schema.rb +++ /dev/null @@ -1,310 +0,0 @@ -require 'active_record' - -ActiveRecord::Schema.verbose = false - -### DATABASE -ActiveRecord::Schema.define do - create_table :people, force: true do |t| - t.string :name - t.string :email - t.datetime :date_joined - t.belongs_to :preferences - t.integer :hair_cut_id, index: true - t.boolean :book_admin, default: false - t.boolean :special, default: false - t.timestamps null: false - end - - create_table :author_details, force: true do |t| - t.integer :person_id - t.string :author_stuff - end - - create_table :posts, force: true do |t| - t.string :title, length: 255 - t.text :body - t.integer :author_id - t.integer :parent_post_id - t.belongs_to :section, index: true - t.timestamps null: false - end - - create_table :comments, force: true do |t| - t.text :body - t.belongs_to :post, index: true - t.integer :author_id - t.timestamps null: false - end - - create_table :companies, force: true do |t| - t.string :type - t.string :name - t.string :address - t.timestamps null: false - end - - create_table :tags, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :sections, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :posts_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :posts_tags, [:post_id, :tag_id], unique: true - - create_table :special_post_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :special_post_tags, [:post_id, :tag_id], unique: true - - create_table :comments_tags, force: true do |t| - t.references :comment, :tag, index: true - end - - create_table :iso_currencies, id: false, force: true do |t| - t.string :code, limit: 3, null: false - t.string :name - t.string :country_name - t.string :minor_unit - t.timestamps null: false - end - add_index :iso_currencies, :code, unique: true - - create_table :expense_entries, force: true do |t| - t.string :currency_code, limit: 3, null: false - t.integer :employee_id, null: false - t.decimal :cost, precision: 12, scale: 4, null: false - t.date :transaction_date - t.timestamps null: false - end - - create_table :planets, force: true do |t| - t.string :name - t.string :description - t.integer :planet_type_id - end - - create_table :planets_tags, force: true do |t| - t.references :planet, :tag, index: true - end - add_index :planets_tags, [:planet_id, :tag_id], unique: true - - create_table :planet_types, force: true do |t| - t.string :name - end - - create_table :moons, force: true do |t| - t.string :name - t.string :description - t.integer :planet_id - t.timestamps null: false - end - - create_table :craters, id: false, force: true do |t| - t.string :code - t.string :description - t.integer :moon_id - t.timestamps null: false - end - - create_table :preferences, force: true do |t| - t.integer :person_id - t.boolean :advanced_mode, default: false - t.timestamps null: false - end - - create_table :facts, force: true do |t| - t.integer :person_id - t.string :spouse_name - t.text :bio - t.float :quality_rating - t.decimal :salary, precision: 12, scale: 2 - t.datetime :date_time_joined - t.date :birthday - t.time :bedtime - t.binary :photo, limit: 1.kilobyte - t.boolean :cool - t.timestamps null: false - end - - create_table :books, force: true do |t| - t.string :title - t.string :isbn - t.boolean :banned, default: false - t.timestamps null: false - end - - create_table :book_authors, force: true do |t| - t.integer :book_id - t.integer :person_id - end - - create_table :book_comments, force: true do |t| - t.text :body - t.belongs_to :book, index: true - t.integer :author_id - t.boolean :approved, default: true - t.timestamps null: false - end - - create_table :customers, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :purchase_orders, force: true do |t| - t.date :order_date - t.date :requested_delivery_date - t.date :delivery_date - t.integer :customer_id - t.string :delivery_name - t.string :delivery_address_1 - t.string :delivery_address_2 - t.string :delivery_city - t.string :delivery_state - t.string :delivery_postal_code - t.float :delivery_fee - t.float :tax - t.float :total - t.timestamps null: false - end - - create_table :order_flags, force: true do |t| - t.string :name - end - - create_table :purchase_orders_order_flags, force: true do |t| - t.references :purchase_order, :order_flag, index: true - end - add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" - - create_table :line_items, force: true do |t| - t.integer :purchase_order_id - t.string :part_number - t.string :quantity - t.float :item_cost - t.timestamps null: false - end - - create_table :hair_cuts, force: true do |t| - t.string :style - end - - create_table :numeros_telefone, force: true do |t| - t.string :numero_telefone - t.timestamps null: false - end - - create_table :categories, force: true do |t| - t.string :name - t.string :status, limit: 10 - t.timestamps null: false - end - - create_table :pictures, force: true do |t| - t.string :name - t.integer :imageable_id - t.string :imageable_type - t.timestamps null: false - end - - create_table :documents, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :products, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :vehicles, force: true do |t| - t.string :type - t.string :make - t.string :model - t.string :length_at_water_line - t.string :drive_layout - t.string :serial_number - t.integer :person_id - t.timestamps null: false - end - - create_table :makes, force: true do |t| - t.string :model - t.timestamps null: false - end - - # special cases - fields that look like they should be reserved names - create_table :hrefs, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :links, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :web_pages, force: true do |t| - t.string :href - t.string :link - t.timestamps null: false - end - - create_table :questionables, force: true do |t| - t.timestamps null: false - end - - create_table :boxes, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :things, force: true do |t| - t.string :name - t.references :user - t.references :box - - t.timestamps null: false - end - - create_table :users, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :related_things, force: true do |t| - t.string :name - t.references :from, references: :thing - t.references :to, references: :thing - - t.timestamps null: false - end - - create_table :questions, force: true do |t| - t.string :text - end - - create_table :answers, force: true do |t| - t.references :question - t.integer :respondent_id - t.string :respondent_type - t.string :text - end - - create_table :patients, force: true do |t| - t.string :name - end - - create_table :doctors, force: true do |t| - t.string :name - end - - # special cases -end \ No newline at end of file diff --git a/test/support/active_record/setup.rb b/test/support/active_record/setup.rb deleted file mode 100644 index 20aae25f5..000000000 --- a/test/support/active_record/setup.rb +++ /dev/null @@ -1,29 +0,0 @@ - -JSONAPI.configuration.default_record_accessor_klass = JSONAPI::ActiveRecordRecordAccessor - -TestApp.class_eval do - config.active_record.schema_format = :none - - if Rails::VERSION::MAJOR >= 5 - config.active_support.halt_callback_chains_on_return_false = false - config.active_record.time_zone_aware_types = [:time, :datetime] - config.active_record.belongs_to_required_by_default = false - end -end - -class Minitest::Test - include ActiveRecord::TestFixtures - - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all -end - -class ActiveSupport::TestCase - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all -end - -class ActionDispatch::IntegrationTest - self.fixture_path = "#{Rails.root}/fixtures" - fixtures :all -end \ No newline at end of file diff --git a/test/support/controllers_resources_processors.rb b/test/support/controllers_resources_processors.rb index f3305f304..8d113d1f3 100644 --- a/test/support/controllers_resources_processors.rb +++ b/test/support/controllers_resources_processors.rb @@ -816,12 +816,8 @@ class BookResource < JSONAPI::Resource filter :banned, apply: :apply_filter_banned class << self - def books - Book.arel_table - end - def not_banned_books - books[:banned].eq(false) + {banned: false} end def records(options = {}) @@ -866,12 +862,12 @@ class BookCommentResource < JSONAPI::Resource } class << self - def book_comments - BookComment.arel_table - end + # def book_comments + # BookComment.arel_table + # end def approved_comments(approved = true) - book_comments[:approved].eq(approved) + {approved: approved} end def records(options = {}) @@ -962,8 +958,17 @@ class CommentResource < CommentResource; end class PostResource < PostResource # Test caching with SQL fragments + # --- + # This is the only resource in the test cases that has an ORM specific implementation + # Rather then extracting this out, let's just keep the logic here until we have more ORM-specific + # resources and then we can move this PostResource to a "resources.rb" for each ORM type. + # That seems like overkill and too much indirection for now, so keeping all resources in one spot. def self.records(options = {}) - _model_class.all.joins('INNER JOIN people on people.id = author_id') + if _model_class.respond_to?(:with_sql) + _model_class.association_join(:author).select_all(:posts) + else + _model_class.all.joins('INNER JOIN people on people.id = author_id') + end end end @@ -1104,12 +1109,6 @@ class PersonResource < JSONAPI::Resource end end -module Legacy - class FlatPost < ActiveRecord::Base - self.table_name = "posts" - end -end - class FlatPostResource < JSONAPI::Resource model_name "Legacy::FlatPost", add_model_hint: false diff --git a/test/support/database.yml b/test/support/database/config.yml similarity index 100% rename from test/support/database.yml rename to test/support/database/config.yml diff --git a/test/support/database/generator.rb b/test/support/database/generator.rb new file mode 100644 index 000000000..17d22216d --- /dev/null +++ b/test/support/database/generator.rb @@ -0,0 +1,359 @@ +# In order to simplify testing of different ORMs and reduce differences in their schema +# generators, we use ActiveRecord::Schema to define our schema for readability and editing purposes. +# When running tests, all different ORMs (Sequel + ActiveRecord) will use the schema.sql to run +# their specs, ensuring a) a consistent test environment and b) easier adding of orms in the future. + +require 'active_support' +require 'active_record' +require 'yaml' + +ActiveSupport.eager_load! + +connection_spec = YAML.load_file(File.expand_path('../../database/config.yml', __FILE__))["test"] + +begin + ActiveRecord::Base.establish_connection(connection_spec) + + ActiveRecord::Schema.verbose = false + + puts "Loading schema into #{connection_spec["database"]}" + + ActiveRecord::Schema.define do + create_table :people, force: true do |t| + t.string :name + t.string :email + t.datetime :date_joined + t.belongs_to :preferences + t.integer :hair_cut_id, index: true + t.boolean :book_admin, default: false + t.boolean :special, default: false + t.timestamps null: false + end + + create_table :author_details, force: true do |t| + t.integer :person_id + t.string :author_stuff + end + + create_table :posts, force: true do |t| + t.string :title, length: 255 + t.text :body + t.integer :author_id + t.integer :parent_post_id + t.belongs_to :section, index: true + t.timestamps null: false + end + + create_table :comments, force: true do |t| + t.text :body + t.belongs_to :post, index: true + t.integer :author_id + t.timestamps null: false + end + + create_table :companies, force: true do |t| + t.string :type + t.string :name + t.string :address + t.timestamps null: false + end + + create_table :tags, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :sections, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :posts_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :posts_tags, [:post_id, :tag_id], unique: true + + create_table :special_post_tags, force: true do |t| + t.references :post, :tag, index: true + end + add_index :special_post_tags, [:post_id, :tag_id], unique: true + + create_table :comments_tags, force: true do |t| + t.references :comment, :tag, index: true + end + + create_table :iso_currencies, id: false, force: true do |t| + t.string :code, limit: 3, null: false + t.string :name + t.string :country_name + t.string :minor_unit + t.timestamps null: false + end + add_index :iso_currencies, :code, unique: true + + create_table :expense_entries, force: true do |t| + t.string :currency_code, limit: 3, null: false + t.integer :employee_id, null: false + t.decimal :cost, precision: 12, scale: 4, null: false + t.date :transaction_date + t.timestamps null: false + end + + create_table :planets, force: true do |t| + t.string :name + t.string :description + t.integer :planet_type_id + end + + create_table :planets_tags, force: true do |t| + t.references :planet, :tag, index: true + end + add_index :planets_tags, [:planet_id, :tag_id], unique: true + + create_table :planet_types, force: true do |t| + t.string :name + end + + create_table :moons, force: true do |t| + t.string :name + t.string :description + t.integer :planet_id + t.timestamps null: false + end + + create_table :craters, id: false, force: true do |t| + t.string :code + t.string :description + t.integer :moon_id + t.timestamps null: false + end + + create_table :preferences, force: true do |t| + t.integer :person_id + t.boolean :advanced_mode, default: false + t.timestamps null: false + end + + create_table :facts, force: true do |t| + t.integer :person_id + t.string :spouse_name + t.text :bio + t.float :quality_rating + t.decimal :salary, precision: 12, scale: 2 + t.datetime :date_time_joined + t.date :birthday + t.time :bedtime + t.binary :photo, limit: 1.kilobyte + t.boolean :cool + t.timestamps null: false + end + + create_table :books, force: true do |t| + t.string :title + t.string :isbn + t.boolean :banned, default: false + t.timestamps null: false + end + + create_table :book_authors, force: true do |t| + t.integer :book_id + t.integer :person_id + end + + create_table :book_comments, force: true do |t| + t.text :body + t.belongs_to :book, index: true + t.integer :author_id + t.boolean :approved, default: true + t.timestamps null: false + end + + create_table :customers, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :purchase_orders, force: true do |t| + t.date :order_date + t.date :requested_delivery_date + t.date :delivery_date + t.integer :customer_id + t.string :delivery_name + t.string :delivery_address_1 + t.string :delivery_address_2 + t.string :delivery_city + t.string :delivery_state + t.string :delivery_postal_code + t.float :delivery_fee + t.float :tax + t.float :total + t.timestamps null: false + end + + create_table :order_flags, force: true do |t| + t.string :name + end + + create_table :purchase_orders_order_flags, force: true do |t| + t.references :purchase_order, :order_flag, index: true + end + add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" + + create_table :line_items, force: true do |t| + t.integer :purchase_order_id + t.string :part_number + t.string :quantity + t.float :item_cost + t.timestamps null: false + end + + create_table :hair_cuts, force: true do |t| + t.string :style + end + + create_table :numeros_telefone, force: true do |t| + t.string :numero_telefone + t.timestamps null: false + end + + create_table :categories, force: true do |t| + t.string :name + t.string :status, limit: 10 + t.timestamps null: false + end + + create_table :pictures, force: true do |t| + t.string :name + t.integer :imageable_id + t.string :imageable_type + t.timestamps null: false + end + + create_table :documents, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :products, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :vehicles, force: true do |t| + t.string :type + t.string :make + t.string :model + t.string :length_at_water_line + t.string :drive_layout + t.string :serial_number + t.integer :person_id + t.timestamps null: false + end + + create_table :makes, force: true do |t| + t.string :model + t.timestamps null: false + end + + # special cases - fields that look like they should be reserved names + create_table :hrefs, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :links, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :web_pages, force: true do |t| + t.string :href + t.string :link + t.timestamps null: false + end + + create_table :questionables, force: true do |t| + t.timestamps null: false + end + + create_table :boxes, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :things, force: true do |t| + t.string :name + t.references :user + t.references :box + + t.timestamps null: false + end + + create_table :users, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :related_things, force: true do |t| + t.string :name + t.references :from, references: :thing + t.references :to, references: :thing + + t.timestamps null: false + end + + create_table :questions, force: true do |t| + t.string :text + end + + create_table :answers, force: true do |t| + t.references :question + t.integer :respondent_id + t.string :respondent_type + t.string :text + end + + create_table :patients, force: true do |t| + t.string :name + end + + create_table :doctors, force: true do |t| + t.string :name + end + + # special cases + end + + class FixtureGenerator + include ActiveRecord::TestFixtures + self.fixture_path = File.expand_path('../fixtures', __FILE__) + fixtures :all + + def self.load_fixtures + require_relative '../inflections' + require_relative '../active_record/models' + ActiveRecord::Base.connection.disable_referential_integrity do + FixtureGenerator.new.send(:load_fixtures, ActiveRecord::Base) + end + end + end + + puts "Loading fixture data into #{connection_spec["database"]}" + + FixtureGenerator.load_fixtures + + puts "Dumping data into data.sql" + + File.open(File.expand_path('../dump.sql', __FILE__), "w") do |f| + `sqlite3 test_db .tables`.split(/\s+/).each do |table_name| + f << %{DROP TABLE IF EXISTS "#{table_name}";\n} + puts "Dumping data from #{table_name}..." + f << `sqlite3 #{connection_spec["database"]} ".dump #{table_name}"` + end.join("\n") + f << "PRAGMA foreign_keys=ON;" # reenable foreign_keys + end + + puts "Done!" +ensure + File.delete(connection_spec["database"]) if File.exists?(connection_spec["database"]) +end \ No newline at end of file diff --git a/test/support/inflections.rb b/test/support/inflections.rb new file mode 100644 index 000000000..9e313956b --- /dev/null +++ b/test/support/inflections.rb @@ -0,0 +1,6 @@ +# These come from the model definitions and are required for fixture creation as well +# as test running. +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.uncountable 'preferences' + inflect.irregular 'numero_telefone', 'numeros_telefone' +end \ No newline at end of file diff --git a/test/support/sequel/app_config.rb b/test/support/sequel/app_config.rb new file mode 100644 index 000000000..560c111a6 --- /dev/null +++ b/test/support/sequel/app_config.rb @@ -0,0 +1 @@ +JSONAPI.configuration.default_record_accessor_klass = JSONAPI::SequelRecordAccessor diff --git a/test/support/sequel/import_schema.rb b/test/support/sequel/import_schema.rb new file mode 100644 index 000000000..872e6ab66 --- /dev/null +++ b/test/support/sequel/import_schema.rb @@ -0,0 +1,6 @@ +statements = File.read(File.expand_path('../../database/dump.sql', __FILE__)).split(/;$/) +statements.pop # the last empty statement + +statements.each do |statement| + Sequel::Model.db[statement] +end \ No newline at end of file diff --git a/test/support/sequel/models.rb b/test/support/sequel/models.rb index 2c132f740..c55f8d94c 100644 --- a/test/support/sequel/models.rb +++ b/test/support/sequel/models.rb @@ -1,28 +1,19 @@ -require 'sequel' -require 'jsonapi-resources' -require_relative 'schema' - -config = Rails.configuration.database_configuration["test"] -config["adapter"] = "sqlite" if config["adapter"]=="sqlite3" -Sequel.connect(config) - Sequel::Model.class_eval do plugin :validation_class_methods plugin :hook_class_methods plugin :timestamps, update_on_create: true - plugin :single_table_inheritance, :type -end + plugin :association_pks + plugin :association_dependencies + plugin :polymorphic -ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.uncountable 'preferences' - inflect.irregular 'numero_telefone', 'numeros_telefone' + db.integer_booleans = false # Fixtures use 't' and 'f' for true and false (ActiveRecord convention) end ### MODELS class Person < Sequel::Model - one_to_many :posts, key: 'author_id' - one_to_many :comments, key: 'author_id' - one_to_many :expense_entries, key: 'employee_id', dependent: :restrict_with_exception + one_to_many :posts, key: :author_id + one_to_many :comments, key: :author_id + one_to_many :expense_entries, key: :employee_id, dependent: :restrict_with_exception one_to_many :vehicles many_to_one :preferences many_to_one :hair_cut @@ -30,28 +21,28 @@ class Person < Sequel::Model many_to_many :books, join_table: :book_authors - one_to_many :even_posts, conditions: 'posts.id % 2 = 0', class: 'Post', key: 'author_id' - one_to_many :odd_posts, conditions: 'posts.id % 2 = 1', class: 'Post', key: 'author_id' + one_to_many :even_posts, conditions: 'posts.id % 2 = 0', class: 'Post', key: :author_id + one_to_many :odd_posts, conditions: 'posts.id % 2 = 1', class: 'Post', key: :author_id ### Validations - validates_presence_of :name, :date_joined + validates_presence_of :name, :date_joined, message: "can't be blank" end class AuthorDetail < Sequel::Model - many_to_one :author, class: 'Person', key: 'person_id' + many_to_one :author, class: 'Person', key: :person_id end class Post < Sequel::Model - many_to_one :author, class: 'Person', key: 'author_id' - many_to_one :writer, class: 'Person', key: 'author_id' + many_to_one :author, class: 'Person', key: :author_id + many_to_one :writer, class: 'Person', key: :author_id one_to_many :comments many_to_many :tags, join_table: :posts_tags one_to_many :special_post_tags, source: :tag - one_to_many :special_tags, through: :special_post_tags, source: :tag + many_to_many :special_tags, join_table: :special_post_tags, right_key: :tag_id many_to_one :section - one_to_one :parent_post, class: 'Post', key: 'parent_post_id' + one_to_one :parent_post, class: 'Post', key: :parent_post_id - validates_presence_of :author + validates_presence_of :author, message: "can't be blank" validates_length_of :title, maximum: 35 before_destroy :destroy_callback @@ -59,16 +50,46 @@ class Post < Sequel::Model def destroy_callback if title == "can't destroy me" errors.add(:title, "can't destroy me") - - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: + false end end + + # For specs + def tag_ids + tag_pks + end + + # For specs + def comment_ids + comment_pks + end + +end + +class PostWithBadAfterSave < Sequel::Model + set_dataset :posts + after_save :do_some_after_save_stuff + + def do_some_after_save_stuff + errors[:base] << 'Boom! Error added in after_save callback.' + raise Sequel::ValidationFailed.new(self) + end +end + +# Sequel Does Not Support Validation Context +# class PostWithCustomValidationContext < Sequel::Model +# set_dataset :posts +# validate :api_specific_check, on: :json_api_create +# +# def api_specific_check +# errors[:base] << 'Record is invalid' +# end +# end + +module Legacy + class FlatPost < Sequel::Model + set_dataset :posts + end end class SpecialPostTag < Sequel::Model @@ -77,12 +98,13 @@ class SpecialPostTag < Sequel::Model end class Comment < Sequel::Model - many_to_one :author, class: 'Person', key: 'author_id' + many_to_one :author, class: 'Person', key: :author_id many_to_one :post many_to_many :tags, join_table: :comments_tags end class Company < Sequel::Model + plugin :single_table_inheritance, :type end class Firm < Company @@ -115,12 +137,12 @@ class Cat < Sequel::Model class IsoCurrency < Sequel::Model set_primary_key :code - # one_to_many :expense_entries, key: 'currency_code' + # one_to_many :expense_entries, key: :currency_code end class ExpenseEntry < Sequel::Model - many_to_one :employee, class: 'Person', key: 'employee_id' - many_to_one :iso_currency, key: 'currency_code' + many_to_one :employee, class: 'Person', key: :employee_id + many_to_one :iso_currency, key: :currency_code end class Planet < Sequel::Model @@ -132,17 +154,9 @@ class Planet < Sequel::Model # Test model callback cancelling save before_save :check_not_pluto + # Pluto can't be a planet, so cancel the save by returning false def check_not_pluto - # Pluto can't be a planet, so cancel the save - if name.downcase == 'pluto' - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - end + name.downcase != 'pluto' end end @@ -163,11 +177,11 @@ class Crater < Sequel::Model end class Preferences < Sequel::Model - one_to_one :author, class: 'Person', :inverse_of => 'preferences' + one_to_one :author, class: 'Person', :reciprocal => :preferences end class Fact < Sequel::Model - validates_presence_of :spouse_name, :bio + validates_presence_of :spouse_name, :bio, message: "can't be blank" end class Like < Sequel::Model @@ -188,7 +202,7 @@ def initialize(id = nil, name = nil) attr_accessor :id, :name - def destroy + def destroy(options={}) $breed_data.remove(@id) end @@ -211,22 +225,18 @@ class Book < Sequel::Model one_to_many :book_comments one_to_many :approved_book_comments, conditions: {approved: true}, class: "BookComment" - many_to_many :authors, join_table: :book_authors, class: "Person" + many_to_many :authors, join_table: :book_authors, right_key: :person_id, class: "Person" end class BookComment < Sequel::Model - many_to_one :author, class: 'Person', key: 'author_id' + many_to_one :author, class: 'Person', key: :author_id many_to_one :book - def before_save - debugger - end - def self.for_user(current_user) records = self # Hide the unapproved comments from people who are not book admins unless current_user && current_user.book_admin - records = records.where(approved: true) + records = records.where(approved: 't') end records end @@ -261,7 +271,7 @@ class Customer < Sequel::Model class PurchaseOrder < Sequel::Model many_to_one :customer one_to_many :line_items - one_to_many :admin_line_items, class: 'LineItem', key: 'purchase_order_id' + one_to_many :admin_line_items, class: 'LineItem', key: :purchase_order_id many_to_many :order_flags, join_table: :purchase_orders_order_flags @@ -288,6 +298,7 @@ class Picture < Sequel::Model class Vehicle < Sequel::Model many_to_one :person + plugin :single_table_inheritance, :type end class Car < Vehicle @@ -297,14 +308,14 @@ class Boat < Vehicle end class Document < Sequel::Model - one_to_many :pictures, as: :imageable + one_to_many :pictures, as: :imageable, reciprocal_type: :many_to_one end class Document::Topic < Document end class Product < Sequel::Model - one_to_one :picture, as: :imageable + one_to_one :picture, as: :imageable, reciprocal_type: :many_to_one end class Make < Sequel::Model @@ -326,7 +337,7 @@ class Thing < Sequel::Model many_to_one :user one_to_many :related_things, key: :from_id - one_to_many :things, through: :related_things, source: :to + many_to_many :things, join_table: :related_things, left_key: :from_id, right_key: :to_id end class RelatedThing < Sequel::Model diff --git a/test/support/sequel/rollback.rb b/test/support/sequel/rollback.rb new file mode 100644 index 000000000..655af72f8 --- /dev/null +++ b/test/support/sequel/rollback.rb @@ -0,0 +1,22 @@ +module Minitest + module Rollback + + def before_setup + Sequel::Model.db.synchronize do |conn| + Sequel::Model.db.send(:add_transaction, conn, {}) + Sequel::Model.db.send(:begin_transaction, conn) + end + super + end + + def after_teardown + super + Sequel::Model.db.synchronize {|conn| Sequel::Model.db.send(:rollback_transaction, conn) } + end + + end + + class Test + include Rollback + end +end \ No newline at end of file diff --git a/test/support/sequel/schema.rb b/test/support/sequel/schema.rb deleted file mode 100644 index 2b76e9b11..000000000 --- a/test/support/sequel/schema.rb +++ /dev/null @@ -1,310 +0,0 @@ -require 'active_record' - -ActiveRecord::Schema.verbose = false - -### DATABASE -ActiveRecord::Schema.define do - create_table :people, force: true do |t| - t.string :name - t.string :email - t.datetime :date_joined - t.belongs_to :preferences - t.integer :hair_cut_id, index: true - t.boolean :book_admin, default: false - t.boolean :special, default: false - t.timestamps null: false - end - - create_table :author_details, force: true do |t| - t.integer :person_id - t.string :author_stuff - end - - create_table :posts, force: true do |t| - t.string :title, length: 255 - t.text :body - t.integer :author_id - t.integer :parent_post_id - t.belongs_to :section, index: true - t.timestamps null: false - end - - create_table :comments, force: true do |t| - t.text :body - t.belongs_to :post, index: true - t.integer :author_id - t.timestamps null: false - end - - create_table :companies, force: true do |t| - t.string :type - t.string :name - t.string :address - t.timestamps null: false - end - - create_table :tags, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :sections, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :posts_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :posts_tags, [:post_id, :tag_id], unique: true - - create_table :special_post_tags, force: true do |t| - t.references :post, :tag, index: true - end - add_index :special_post_tags, [:post_id, :tag_id], unique: true - - create_table :comments_tags, force: true do |t| - t.references :comment, :tag, index: true - end - - create_table :iso_currencies, id: false, force: true do |t| - t.string :code, limit: 3, null: false - t.string :name - t.string :country_name - t.string :minor_unit - t.timestamps null: false - end - add_index :iso_currencies, :code, unique: true - - create_table :expense_entries, force: true do |t| - t.string :currency_code, limit: 3, null: false - t.integer :employee_id, null: false - t.decimal :cost, precision: 12, scale: 4, null: false - t.date :transaction_date - t.timestamps null: false - end - - create_table :planets, force: true do |t| - t.string :name - t.string :description - t.integer :planet_type_id - end - - create_table :planets_tags, force: true do |t| - t.references :planet, :tag, index: true - end - add_index :planets_tags, [:planet_id, :tag_id], unique: true - - create_table :planet_types, force: true do |t| - t.string :name - end - - create_table :moons, force: true do |t| - t.string :name - t.string :description - t.integer :planet_id - t.timestamps null: false - end - - create_table :craters, id: false, force: true do |t| - t.string :code - t.string :description - t.integer :moon_id - t.timestamps null: false - end - - create_table :preferences, force: true do |t| - t.integer :person_id - t.boolean :advanced_mode, default: false - t.timestamps null: false - end - - create_table :facts, force: true do |t| - t.integer :person_id - t.string :spouse_name - t.text :bio - t.float :quality_rating - t.decimal :salary, precision: 12, scale: 2 - t.datetime :date_time_joined - t.date :birthday - t.time :bedtime - t.binary :photo, limit: 1.kilobyte - t.boolean :cool - t.timestamps null: false - end - - create_table :books, force: true do |t| - t.string :title - t.string :isbn - t.boolean :banned, default: false - t.timestamps null: false - end - - create_table :book_authors, force: true do |t| - t.integer :book_id - t.integer :person_id - end - - create_table :book_comments, force: true do |t| - t.text :body - t.belongs_to :book, index: true - t.integer :author_id - t.boolean :approved, default: true - t.timestamps null: false - end - - create_table :customers, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :purchase_orders, force: true do |t| - t.date :order_date - t.date :requested_delivery_date - t.date :delivery_date - t.integer :customer_id - t.string :delivery_name - t.string :delivery_address_1 - t.string :delivery_address_2 - t.string :delivery_city - t.string :delivery_state - t.string :delivery_postal_code - t.float :delivery_fee - t.float :tax - t.float :total - t.timestamps null: false - end - - create_table :order_flags, force: true do |t| - t.string :name - end - - create_table :purchase_orders_order_flags, force: true do |t| - t.references :purchase_order, :order_flag, index: true - end - add_index :purchase_orders_order_flags, [:purchase_order_id, :order_flag_id], unique: true, name: "po_flags_idx" - - create_table :line_items, force: true do |t| - t.integer :purchase_order_id - t.string :part_number - t.string :quantity - t.float :item_cost - t.timestamps null: false - end - - create_table :hair_cuts, force: true do |t| - t.string :style - end - - create_table :numeros_telefone, force: true do |t| - t.string :numero_telefone - t.timestamps null: false - end - - create_table :categories, force: true do |t| - t.string :name - t.string :status, limit: 10 - t.timestamps null: false - end - - create_table :pictures, force: true do |t| - t.string :name - t.integer :imageable_id - t.string :imageable_type - t.timestamps null: false - end - - create_table :documents, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :products, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :vehicles, force: true do |t| - t.string :type - t.string :make - t.string :model - t.string :length_at_water_line - t.string :drive_layout - t.string :serial_number - t.integer :person_id - t.timestamps null: false - end - - create_table :makes, force: true do |t| - t.string :model - t.timestamps null: false - end - - # special cases - fields that look like they should be reserved names - create_table :hrefs, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :links, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :web_pages, force: true do |t| - t.string :href - t.string :link - t.timestamps null: false - end - - create_table :questionables, force: true do |t| - t.timestamps null: false - end - - create_table :boxes, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :things, force: true do |t| - t.string :name - t.references :user - t.references :box - - t.timestamps null: false - end - - create_table :users, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :related_things, force: true do |t| - t.string :name - t.references :from, references: :thing - t.references :to, references: :thing - - t.timestamps null: false - end - - create_table :questions, force: true do |t| - t.string :text - end - - create_table :answers, force: true do |t| - t.references :question - t.integer :respondent_id - t.string :respondent_type - t.string :text - end - - create_table :patients, force: true do |t| - t.string :name - end - - create_table :doctors, force: true do |t| - t.string :name - end - - # special cases -end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index 6893f2434..035868a92 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -18,19 +18,16 @@ end end -ENV["ORM"] = "active_record" +ENV["ORM"] ||= "active_record" +require 'rails' require_relative "support/#{ENV["ORM"]}/initialize" +require_relative "support/inflections" require 'rails/test_help' require 'minitest/mock' require 'jsonapi-resources' require 'pry' -require File.expand_path('../helpers/value_matchers', __FILE__) -require File.expand_path('../helpers/assertions', __FILE__) -require File.expand_path('../helpers/functional_helpers', __FILE__) -require File.expand_path('../helpers/configuration_helpers', __FILE__) - Rails.env = 'test' I18n.load_path += Dir[File.expand_path("../../locales/*.yml", __FILE__)] @@ -53,7 +50,7 @@ class TestApp < Rails::Application config.active_support.test_order = :random - config.paths["config/database"] = "support/database.yml" + config.paths["config/database"] = "support/database/config.yml" ActiveSupport::Deprecation.silenced = true @@ -191,7 +188,11 @@ def assign_parameters(routes, controller_path, action, parameters, generated_pat def assert_query_count(expected, msg = nil, &block) @queries = [] - callback = lambda {|_, _, _, _, payload| @queries.push payload[:sql] } + callback = lambda do |_, _, _, _, payload| + # Ignore ORM methods to introspect column names and primary keys. + # This happens when the Model first has methods accessed on it. + @queries.push(payload[:sql]) unless payload[:sql]=~/sqlite_temp_master|PRAGMA/ + end ActiveSupport::Notifications.subscribed(callback, "sql.#{ENV["ORM"]}", &block) show_queries unless expected == @queries.size @@ -205,8 +206,22 @@ def show_queries end end +# For debugging purposes, put in global scope to be used anywhere in the code. +def show_sql(&block) + output_sql = lambda {|_, _, _, _, payload| puts payload[:sql] } + ActiveSupport::Notifications.subscribed(output_sql, "sql.#{ENV["ORM"]}", &block) +end + TestApp.initialize! +require_relative "support/#{ENV["ORM"]}/app_config" + +# We used to have the schema in the ActiveRecord schema creation format, but then we would need +# to reimplement the schema bulider in other ORMs that we are testing. We could always require ActiveRecord +# for the purposes of schema creation, but then we would have to try to remove ActiveRecord from the global +# namespace to really have no side-effects when running the specs. The goal of running the specs with other +# orms is to not have ActiveRecord required in at all. +require_relative "support/#{ENV["ORM"]}/import_schema" require_relative "support/#{ENV["ORM"]}/models" require_relative "support/controllers_resources_processors" @@ -410,11 +425,18 @@ class CatResource < JSONAPI::Resource # Ensure backward compatibility with Minitest 4 Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) +require_relative 'helpers/assertions' +require_relative 'helpers/value_matchers' +require_relative 'helpers/functional_helpers' +require_relative 'helpers/configuration_helpers' +require_relative 'helpers/record_accessor_helpers' + class Minitest::Test include Helpers::Assertions include Helpers::ValueMatchers include Helpers::FunctionalHelpers include Helpers::ConfigurationHelpers + include Helpers::RecordAccessorHelpers def run_in_transaction? true @@ -492,15 +514,15 @@ def assert_cacheable_get(action, *args) orig_queries = @queries.try(:dup) orig_request_headers = @request.headers.dup - ar_resource_klass = nil + resource_class = nil modes = {none: [], all: :all} if @controller.class.included_modules.include?(JSONAPI::ActsAsResourceController) - ar_resource_klass = @controller.send(:resource_klass) - if ar_resource_klass._model_class.respond_to?(:arel_table) - modes[:root_only] = [ar_resource_klass] - modes[:all_but_root] = {except: [ar_resource_klass]} + resource_class = @controller.send(:resource_klass) + if resource_class.model_class_compatible_with_record_accessor? + modes[:root_only] = [resource_class] + modes[:all_but_root] = {except: [resource_class]} else - ar_resource_klass = nil + resource_class = nil end end @@ -533,6 +555,11 @@ def assert_cacheable_get(action, *args) response.status, "Cache (mode: #{mode}) #{phase} response status must match normal response" ) + # puts "non_caching_response.pretty_inspect" + # puts non_caching_response.pretty_inspect + # puts "json_response_sans_backtraces.pretty_inspect" + # puts json_response_sans_backtraces.pretty_inspect + # debugger if @response.body.include?("error") assert_equal( non_caching_response.pretty_inspect, json_response_sans_backtraces.pretty_inspect, @@ -549,7 +576,7 @@ def assert_cacheable_get(action, *args) if mode == :all # TODO Should also be caching :show_related_resource (non-plural) action if [:index, :show, :show_related_resources].include?(action) - if ar_resource_klass && response.status == 200 && json_response["data"].try(:size) > 0 + if resource_class && response.status == 200 && json_response["data"].try(:size) > 0 assert_operator( cache_activity[:warmup][:total][:misses], :>, @@ -641,4 +668,7 @@ def unformat(value) end end -require_relative "support/#{ENV["ORM"]}/setup" \ No newline at end of file +# Implement rollback functionality for each spec. The schema and data is loaded +# initially via the support/database/dump.sql file and each spec simply runs in a transaction +# and returns the database to it's current state. +require_relative "support/#{ENV["ORM"]}/rollback" \ No newline at end of file diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index fc918f039..60a623d1b 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -8,25 +8,6 @@ def self.records(options) end end -class PostWithBadAfterSave < ActiveRecord::Base - self.table_name = 'posts' - after_save :do_some_after_save_stuff - - def do_some_after_save_stuff - errors[:base] << 'Boom! Error added in after_save callback.' - raise ActiveRecord::RecordInvalid.new(self) - end -end - -class PostWithCustomValidationContext < ActiveRecord::Base - self.table_name = 'posts' - validate :api_specific_check, on: :json_api_create - - def api_specific_check - errors[:base] << 'Record is invalid' - end -end - class ArticleWithBadAfterSaveResource < JSONAPI::Resource model_name 'PostWithBadAfterSave' attribute :title @@ -238,15 +219,15 @@ def test_duplicate_attribute_name end def test_find_with_customized_base_records - author = Person.find(1) + author = find_first(Person, 1) posts = ArticleResource.find([], context: author).map(&:_model) - assert(posts.include?(Post.find(1))) - refute(posts.include?(Post.find(3))) + assert(posts.include?(find_first(Post, 1))) + refute(posts.include?(find_first(Post, 3))) end def test_records_for - author = Person.find(1) + author = find_first(Person, 1) preferences = Preferences.first refute(preferences == nil) author.update! preferences: preferences @@ -263,7 +244,7 @@ def test_records_for end def test_records_for_meta_method_for_to_one - author = Person.find(1) + author = find_first(Person, 1) author.update! preferences: Preferences.first author_resource = PersonWithCustomRecordsForRelationshipsResource.new(author, nil) assert_equal(author_resource.class._record_accessor.records_for( @@ -271,7 +252,7 @@ def test_records_for_meta_method_for_to_one end def test_records_for_meta_method_for_to_one_calling_records_for - author = Person.find(1) + author = find_first(Person, 1) author.update! preferences: Preferences.first author_resource = PersonWithCustomRecordsForResource.new(author, nil) assert_equal(author_resource.class._record_accessor.records_for( @@ -279,26 +260,26 @@ def test_records_for_meta_method_for_to_one_calling_records_for end def test_associated_records_meta_method_for_to_many - author = Person.find(1) - author.posts << Post.find(1) + author = find_first(Person, 1) + author.posts << find_first(Post, 1) author_resource = PersonWithCustomRecordsForRelationshipsResource.new(author, nil) assert_equal(author_resource.class._record_accessor.records_for( author_resource, :posts), :records_for_posts) end def test_associated_records_meta_method_for_to_many_calling_records_for - author = Person.find(1) - author.posts << Post.find(1) + author = find_first(Person, 1) + author.posts << find_first(Post, 1) author_resource = PersonWithCustomRecordsForResource.new(author, nil) assert_equal(author_resource.class._record_accessor.records_for( author_resource, :posts), :records_for) end def test_find_by_key_with_customized_base_records - author = Person.find(1) + author = find_first(Person, 1) post = ArticleResource.find_by_key(1, context: author)._model - assert_equal(post, Post.find(1)) + assert_equal(post, find_first(Post, 1)) assert_raises JSONAPI::Exceptions::RecordNotFound do ArticleResource.find_by_key(3, context: author)._model @@ -330,7 +311,7 @@ def test_filter_on_has_one_relationship_id end def test_to_many_relationship_filters - post_resource = PostResource.new(Post.find(1), nil) + post_resource = PostResource.new(find_first(Post, 1), nil) comments = post_resource.comments assert_equal(2, comments.size) @@ -379,7 +360,7 @@ def apply_filters(records, filters, options) end def test_to_many_relationship_sorts - post_resource = PostResource.new(Post.find(1), nil) + post_resource = PostResource.new(find_first(Post, 1), nil) comment_ids = post_resource.comments.map{|c| c._model.id } assert_equal [1,2], comment_ids @@ -450,7 +431,7 @@ def test_build_joins end def test_to_many_relationship_pagination - post_resource = PostResource.new(Post.find(1), nil) + post_resource = PostResource.new(find_first(Post, 1), nil) comments = post_resource.comments assert_equal 2, comments.size @@ -656,7 +637,7 @@ class NoModelAbstractResource < JSONAPI::Resource end def test_correct_error_surfaced_if_validation_errors_in_after_save_callback - post = PostWithBadAfterSave.find(1) + post = find_first(PostWithBadAfterSave, 1) post_resource = ArticleWithBadAfterSaveResource.new(post, nil) err = assert_raises JSONAPI::Exceptions::ValidationErrors do post_resource.replace_fields({:attributes => {:title => 'Some title'}}) @@ -672,7 +653,7 @@ def test_resource_for_model_use_hint end def test_resource_performs_validations_in_custom_context - post = PostWithCustomValidationContext.find(1) + post = find_first(PostWithCustomValidationContext, 1) post_resource = ArticleWithCustomValidationContextResource.new(post, nil) err = assert_raises JSONAPI::Exceptions::ValidationErrors do post_resource._save diff --git a/test/unit/serializer/polymorphic_serializer_test.rb b/test/unit/serializer/polymorphic_serializer_test.rb index bb905fde8..7d9f51750 100644 --- a/test/unit/serializer/polymorphic_serializer_test.rb +++ b/test/unit/serializer/polymorphic_serializer_test.rb @@ -5,7 +5,7 @@ class PolymorphismTest < ActionDispatch::IntegrationTest def setup @pictures = Picture.all - @person = Person.find(1) + @person = find_first(Person, 1) @questions = Question.all diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index 163c39cad..adaf79f10 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -4,10 +4,10 @@ class SerializerTest < ActionDispatch::IntegrationTest def setup - @post = Post.find(1) - @fred = Person.find_by(name: 'Fred Reader') + @post = find_first(Post, 1) + @fred = find_first(Person, name: 'Fred Reader') - @expense_entry = ExpenseEntry.find(1) + @expense_entry = find_first(ExpenseEntry, 1) JSONAPI.configuration.json_key_format = :camelized_key JSONAPI.configuration.route_format = :camelized_route @@ -530,7 +530,7 @@ def test_serializer_include_sub_objects end def test_serializer_keeps_sorted_order_of_objects_with_self_referential_relationships - post1, post2, post3 = Post.find(1), Post.find(2), Post.find(3) + post1, post2, post3 = find_first(Post, 1), find_first(Post, 2), find_first(Post, 3) post1.parent_post = post3 ordered_posts = [post1, post2, post3] serialized_data = JSONAPI::ResourceSerializer.new( @@ -672,7 +672,7 @@ def test_serializer_different_foreign_key def test_serializer_array_of_resources_always_include_to_one_linkage_data posts = [] - Post.find(1, 2).each do |post| + find_all(Post, 1, 2).each do |post| posts.push PostResource.new(post, nil) end @@ -988,7 +988,7 @@ def test_serializer_array_of_resources_always_include_to_one_linkage_data def test_serializer_always_include_to_one_linkage_data_does_not_load_association JSONAPI.configuration.always_include_to_one_linkage_data = true - post = Post.find(1) + post = find_first(Post, 1) resource = Api::V1::PostResource.new(post, nil) JSONAPI::ResourceSerializer.new(Api::V1::PostResource).serialize_to_hash(resource) @@ -1000,7 +1000,7 @@ def test_serializer_always_include_to_one_linkage_data_does_not_load_association def test_serializer_array_of_resources posts = [] - Post.find(1, 2).each do |post| + find_all(Post, 1, 2).each do |post| posts.push PostResource.new(post, nil) end @@ -1275,7 +1275,7 @@ def test_serializer_array_of_resources def test_serializer_array_of_resources_limited_fields posts = [] - Post.find(1, 2).each do |post| + find_all(Post, 1, 2).each do |post| posts.push PostResource.new(post, nil) end @@ -1714,7 +1714,7 @@ def test_serializer_to_one serialized = JSONAPI::ResourceSerializer.new( Api::V5::AuthorResource, include: ['author_detail'] - ).serialize_to_hash(Api::V5::AuthorResource.new(Person.find(1), nil)) + ).serialize_to_hash(Api::V5::AuthorResource.new(find_first(Person, 1), nil)) assert_hash_equals( { @@ -1773,7 +1773,7 @@ def meta(options) serialized = JSONAPI::ResourceSerializer.new( Api::V5::AuthorResource, include: ['author_detail'] - ).serialize_to_hash(Api::V5::AuthorResource.new(Person.find(1), nil)) + ).serialize_to_hash(Api::V5::AuthorResource.new(find_first(Person, 1), nil)) assert_hash_equals( { @@ -2098,7 +2098,7 @@ def test_custom_links_with_if_condition_equals_false def test_custom_links_with_if_condition_equals_true serialized_custom_link_resource = JSONAPI::ResourceSerializer .new(CustomLinkWithIfCondition, base_url: 'http://example.com') - .serialize_to_hash(CustomLinkWithIfCondition.new(Post.find_by(title: "JR Solves your serialization woes!"), {})) + .serialize_to_hash(CustomLinkWithIfCondition.new(find_first(Post, title: "JR Solves your serialization woes!"), {})) custom_link_spec = { data: { @@ -2190,7 +2190,7 @@ def test_custom_links_with_lambda def test_includes_two_relationships_with_same_foreign_key serialized_resource = JSONAPI::ResourceSerializer .new(PersonWithEvenAndOddPostsResource, include: ['even_posts','odd_posts']) - .serialize_to_hash(PersonWithEvenAndOddPostsResource.new(Person.find(1), nil)) + .serialize_to_hash(PersonWithEvenAndOddPostsResource.new(find_first(Person, 1), nil)) assert_hash_equals( {