From 3b208946ff7e95d71ce79687f868998e942b280b Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Thu, 23 Mar 2017 02:50:48 -0700 Subject: [PATCH 1/5] 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 2a30c3d7c5185f3cb78427a50109f482c2dba956 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Fri, 24 Mar 2017 17:49:35 -0700 Subject: [PATCH 2/5] Refactor tests to allow for setup and testing with multiple ORMs --- .gitignore | 1 + jsonapi-resources.gemspec | 6 +- lib/jsonapi/active_record_record_accessor.rb | 528 +++++ lib/jsonapi/resource.rb | 2 +- lib/jsonapi/sequel_record_accessor.rb | 528 +++++ test/fixtures/sequel.rb | 1877 ----------------- .../controllers_resources_processors.rb} | 836 +------- 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 | 32 + 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 | 36 +- test/unit/resource/resource_test.rb | 5 +- 18 files changed, 2531 insertions(+), 2742 deletions(-) create mode 100644 lib/jsonapi/active_record_record_accessor.rb create mode 100644 lib/jsonapi/sequel_record_accessor.rb delete mode 100644 test/fixtures/sequel.rb rename test/{fixtures/active_record.rb => support/controllers_resources_processors.rb} (68%) 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/.gitignore b/.gitignore index 800c71c6a..45f6fdbd2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ test_db test_db-journal .idea *.iml +test/support/database/dump.sql diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index 1bb48eea2..b8d474cb2 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -26,8 +26,10 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'simplecov' spec.add_development_dependency 'pry' spec.add_development_dependency 'concurrent-ruby-ext' - spec.add_development_dependency 'database_cleaner' - spec.add_dependency 'activerecord', '>= 4.1' + 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' spec.add_dependency 'concurrent-ruby' end diff --git a/lib/jsonapi/active_record_record_accessor.rb b/lib/jsonapi/active_record_record_accessor.rb new file mode 100644 index 000000000..8b710a444 --- /dev/null +++ b/lib/jsonapi/active_record_record_accessor.rb @@ -0,0 +1,528 @@ +require 'jsonapi/record_accessor' + +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 + + 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]) + 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 diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 0c09fb7e8..95ea71ebd 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -2,4 +2,4 @@ module JSONAPI class Resource < ActiveRelationResource root_resource end -end \ No newline at end of file +end diff --git a/lib/jsonapi/sequel_record_accessor.rb b/lib/jsonapi/sequel_record_accessor.rb new file mode 100644 index 000000000..bc60085a6 --- /dev/null +++ b/lib/jsonapi/sequel_record_accessor.rb @@ -0,0 +1,528 @@ +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::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]) + 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 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 68% rename from test/fixtures/active_record.rb rename to test/support/controllers_resources_processors.rb index 68bb69fc3..d48867809 100644 --- a/test/fixtures/active_record.rb +++ b/test/support/controllers_resources_processors.rb @@ -1,838 +1,4 @@ -require 'active_record' -require 'jsonapi-resources' - -ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.uncountable 'preferences' - inflect.uncountable 'file_properties' - inflect.irregular 'numero_telefone', 'numeros_telefone' -end - -### DATABASE -ActiveRecord::Schema.define do - create_table :sessions, id: false, force: true do |t| - t.string :id, :limit => 36, :primary_key => true, null: false - t.string :survey_id, :limit => 36, null: false - - t.timestamps - end - - create_table :responses, force: true do |t| - #t.string :id, :limit => 36, :primary_key => true, null: false - - t.string :session_id, limit: 36, null: false - - t.string :type - t.string :question_id, limit: 36 - - t.timestamps - end - - create_table :response_texts, force: true do |t| - t.text :text - t.integer :response_id - - t.timestamps - end - - 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 - t.timestamps null: false - 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.string :nickname - 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 :author_id - t.references :imageable, polymorphic: true, index: true - t.timestamps null: false - end - - create_table :documents, force: true do |t| - t.string :name - t.integer :author_id - t.timestamps null: false - end - - create_table :products, force: true do |t| - t.string :name - t.integer :designer_id - t.timestamps null: false - end - - create_table :file_properties, force: true do |t| - t.string :name - t.timestamps null: false - t.references :fileable, polymorphic: true, index: true - t.belongs_to :tag, index: true - - t.integer :size - 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 - t.timestamps null: false - end - - create_table :answers, force: true do |t| - t.references :question - t.integer :respondent_id - t.string :respondent_type - t.string :text - t.timestamps null: false - end - - create_table :patients, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :doctors, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :painters, force: true do |t| - t.string :name - - t.timestamps null: false - end - - create_table :paintings, force: true do |t| - t.string :title - t.string :category - t.belongs_to :painter - - t.timestamps null: false - end - - create_table :collectors, force: true do |t| - t.string :name - t.belongs_to :painting - end - - create_table :lists, force: true do |t| - t.string :name - end - - create_table :list_items, force: true do |t| - t.belongs_to :list - end - - # special cases - create_table :storages, force: true do |t| - t.string :token, null: false - t.string :name - t.timestamps null: false - end - - create_table :keepers, force: true do |t| - t.string :name - t.string :keepable_type, null: false - t.integer :keepable_id, null: false - t.timestamps null: false - end - - create_table :access_cards, force: true do |t| - t.string :token, null: false - t.string :security_level - t.timestamps null: false - end - - create_table :workers, force: true do |t| - t.string :name - t.integer :access_card_id, null: false - t.timestamps null: false - end - - create_table :agencies, force: true do |t| - t.string :name - t.timestamps null: false - end - - create_table :indicators, force: true do |t| - t.string :name - t.string :import_id - t.integer :agency_id, null: false - t.timestamps null: false - end - - create_table :widgets, force: true do |t| - t.string :name - t.string :indicator_import_id, null: false - t.timestamps null: false - end - - create_table :robots, force: true do |t| - t.string :name - t.integer :version - t.timestamps null: false - end -end - -### MODELS -class Session < ActiveRecord::Base - self.primary_key = "id" - has_many :responses -end - -class Response < ActiveRecord::Base - belongs_to :session - has_one :paragraph, :class_name => "ResponseText::Paragraph" - - def response_type - case self.type - when "Response::SingleTextbox" - "single_textbox" - else - "question" - end - end - def response_type=type - self.type = case type - when "single_textbox" - "Response::SingleTextbox" - else - "Response" - end - end -end - -class Response::SingleTextbox < Response - has_one :paragraph, :class_name => "ResponseText::Paragraph", :foreign_key => :response_id -end - -class ResponseText < ActiveRecord::Base -end - -class ResponseText::Paragraph < ResponseText -end - -class Person < ActiveRecord::Base - has_many :posts, foreign_key: 'author_id' - has_many :comments, foreign_key: 'author_id' - has_many :book_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_and_belongs_to_many :not_banned_books, -> { merge(Book.not_banned) }, - class_name: 'Book', - 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' - - has_many :pictures, 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 - belongs_to :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 - case title - when "can't destroy me", "can't destroy me either" - errors.add(:base, "can't destroy me") - - # :nocov: - if Rails::VERSION::MAJOR >= 5 - throw(:abort) - else - return false - end - # :nocov: - when "locked title" - errors.add(:title, "is locked") - - # :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 - - has_and_belongs_to_many :comments, join_table: :comments_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 - include ActiveModel::Model - - 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 - - validates :name, presence: true -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" - - scope :not_banned, -> { - where(banned: false) - } -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.all - # 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 :author, class_name: 'Person', foreign_key: 'author_id' - - belongs_to :imageable, polymorphic: true - belongs_to :document, -> { where( pictures: { imageable_type: 'Document' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' - belongs_to :product, -> { where( pictures: { imageable_type: 'Product' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' - - has_one :file_properties, as: 'fileable' -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 - belongs_to :author, class_name: 'Person', foreign_key: 'author_id' - has_one :file_properties, as: 'fileable' -end - -class Product < ActiveRecord::Base - has_many :pictures, as: :imageable - belongs_to :designer, class_name: 'Person', foreign_key: 'designer_id' - has_one :file_properties, as: 'fileable' -end - -class FileProperties < ActiveRecord::Base - belongs_to :fileable, polymorphic: true - belongs_to :tag -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. class Storage < ActiveRecord::Base has_one :keeper, class_name: 'Keeper', as: :keepable 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..1e2cb1fae --- /dev/null +++ b/test/support/orm/active_record/setup.rb @@ -0,0 +1,32 @@ + +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 + if Rails::VERSION::MINOR >= 2 + config.active_record.sqlite3.represent_boolean_as_integer = true + end + 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 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 b8e6acc40..8aa8fc749 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -21,7 +21,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' @@ -43,7 +45,7 @@ ActiveSupport::Deprecation.silenced = true -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 @@ -54,18 +56,9 @@ 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 - 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 - if Rails::VERSION::MINOR >= 2 - config.active_record.sqlite3.represent_boolean_as_integer = true - end - end + ActiveSupport::Deprecation.silenced = true end module MyEngine @@ -200,7 +193,7 @@ def assert_query_count(expected, msg = nil, &block) 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" @@ -226,7 +219,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 @@ -487,27 +481,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 @@ -562,7 +548,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_all_backtraces @@ -595,7 +581,7 @@ def assert_cacheable_get(action, *args) 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) @@ -735,3 +721,5 @@ def unformat(formatted_route) end end end + +require_relative "support/orm/#{ENV["ORM"]}/setup" diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index 0bb8d1aa7..770abde17 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -431,8 +431,7 @@ def test_key_type_proc end def test_id_attr_deprecation - - ActiveSupport::Deprecation.silenced = false + tmp, ActiveSupport::Deprecation.silenced = ActiveSupport::Deprecation.silenced, false _out, err = capture_io do eval <<-CODE class ProblemResource < JSONAPI::Resource @@ -442,7 +441,7 @@ class ProblemResource < JSONAPI::Resource 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 = true + ActiveSupport::Deprecation.silenced = tmp end def test_id_attr_with_format From fe81351f2f5896784bcb017bbaa9027188d42193 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Fri, 24 Mar 2017 18:55:55 -0700 Subject: [PATCH 3/5] 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 9c9adef75e44fefcb963d17da54edd7c7b826a81 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Mon, 27 Mar 2017 04:30:25 -0700 Subject: [PATCH 4/5] 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 368105abb74675734ba2126b9f75fc558dd542a6 Mon Sep 17 00:00:00 2001 From: Lukasz Sarnacki Date: Mon, 12 Aug 2019 19:03:07 +0200 Subject: [PATCH 5/5] Fixes for original sequel adapter There was an attempt to write sequel adapter in #1030. I took commits from this PR. This commit fixes most of the failures that are result of applying these commits into much newer codebase. Most of the code is also copied from the original PR. --- Rakefile | 4 + jsonapi-resources.gemspec | 1 + lib/jsonapi/active_record_record_accessor.rb | 528 ------------------ test/support/active_record/app_config.rb | 13 + test/support/active_record/import_schema.rb | 11 + .../{orm => }/active_record/initialize.rb | 0 .../support/{orm => }/active_record/models.rb | 160 +++++- test/support/active_record/rollback.rb | 21 + test/support/{orm => }/active_record/setup.rb | 3 - .../controllers_resources_processors.rb | 52 -- test/support/database/config.yml | 5 + test/support/database/generator.rb | 476 ++++++++++++++++ test/support/inflections.rb | 7 + test/support/models.rb | 350 ------------ test/support/orm/active_record/schema.rb | 310 ---------- test/support/orm/test_configurator.rb | 22 - test/support/sequel/app_config.rb | 0 test/support/sequel/import_schema.rb | 6 + test/support/{orm => }/sequel/initialize.rb | 0 test/support/{orm => }/sequel/models.rb | 1 - test/support/sequel/rollback.rb | 22 + test/support/sequel/schema.rb | 310 ---------- test/support/{orm => }/sequel/setup.rb | 5 +- test/test_helper.rb | 19 +- 24 files changed, 715 insertions(+), 1611 deletions(-) delete mode 100644 lib/jsonapi/active_record_record_accessor.rb create mode 100644 test/support/active_record/app_config.rb create mode 100644 test/support/active_record/import_schema.rb rename test/support/{orm => }/active_record/initialize.rb (100%) rename test/support/{orm => }/active_record/models.rb (65%) create mode 100644 test/support/active_record/rollback.rb rename test/support/{orm => }/active_record/setup.rb (89%) create mode 100644 test/support/database/config.yml create mode 100644 test/support/database/generator.rb create mode 100644 test/support/inflections.rb delete mode 100644 test/support/models.rb delete mode 100644 test/support/orm/active_record/schema.rb delete mode 100644 test/support/orm/test_configurator.rb create mode 100644 test/support/sequel/app_config.rb create mode 100644 test/support/sequel/import_schema.rb rename test/support/{orm => }/sequel/initialize.rb (100%) rename test/support/{orm => }/sequel/models.rb (99%) create mode 100644 test/support/sequel/rollback.rb delete mode 100644 test/support/sequel/schema.rb rename test/support/{orm => }/sequel/setup.rb (76%) diff --git a/Rakefile b/Rakefile index 01619ed8e..928a92b0f 100644 --- a/Rakefile +++ b/Rakefile @@ -15,6 +15,10 @@ 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 desc 'Test bug report template' diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index b8d474cb2..9365b24b5 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'sequel' spec.add_development_dependency 'sequel-rails' spec.add_development_dependency 'activerecord', '>= 4.1' + spec.add_development_dependency 'database_cleaner' spec.add_dependency 'activesupport', '>= 4.1' spec.add_dependency 'railties', '>= 4.1' spec.add_dependency 'concurrent-ruby' diff --git a/lib/jsonapi/active_record_record_accessor.rb b/lib/jsonapi/active_record_record_accessor.rb deleted file mode 100644 index 8b710a444..000000000 --- a/lib/jsonapi/active_record_record_accessor.rb +++ /dev/null @@ -1,528 +0,0 @@ -require 'jsonapi/record_accessor' - -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 - - 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]) - 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 diff --git a/test/support/active_record/app_config.rb b/test/support/active_record/app_config.rb new file mode 100644 index 000000000..f33f59f5b --- /dev/null +++ b/test/support/active_record/app_config.rb @@ -0,0 +1,13 @@ +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 + + if Rails::VERSION::MINOR >= 2 + config.active_record.sqlite3.represent_boolean_as_integer = true + end + end +end diff --git a/test/support/active_record/import_schema.rb b/test/support/active_record/import_schema.rb new file mode 100644 index 000000000..07c4ce254 --- /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 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 65% rename from test/support/orm/active_record/models.rb rename to test/support/active_record/models.rb index 021ecae83..6d7c1bf1e 100644 --- a/test/support/orm/active_record/models.rb +++ b/test/support/active_record/models.rb @@ -1,13 +1,44 @@ -require_relative 'schema' +class Session < ActiveRecord::Base + self.primary_key = "id" + has_many :responses +end + +class Response < ActiveRecord::Base + belongs_to :session + has_one :paragraph, :class_name => "ResponseText::Paragraph" -ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.uncountable 'preferences' - inflect.irregular 'numero_telefone', 'numeros_telefone' + def response_type + case self.type + when "Response::SingleTextbox" + "single_textbox" + else + "question" + end + end + def response_type=type + self.type = case type + when "single_textbox" + "Response::SingleTextbox" + else + "Response" + end + end +end + +class Response::SingleTextbox < Response + has_one :paragraph, :class_name => "ResponseText::Paragraph", :foreign_key => :response_id +end + +class ResponseText < ActiveRecord::Base +end + +class ResponseText::Paragraph < ResponseText end class Person < ActiveRecord::Base has_many :posts, foreign_key: 'author_id' has_many :comments, foreign_key: 'author_id' + has_many :book_comments, foreign_key: 'author_id' has_many :expense_entries, foreign_key: 'employee_id', dependent: :restrict_with_exception has_many :vehicles belongs_to :preferences @@ -15,10 +46,15 @@ class Person < ActiveRecord::Base has_one :author_detail has_and_belongs_to_many :books, join_table: :book_authors + has_and_belongs_to_many :not_banned_books, -> { merge(Book.not_banned) }, + class_name: 'Book', + 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' + has_many :pictures, foreign_key: 'author_id' + ### Validations validates :name, presence: true validates :date_joined, presence: true @@ -36,7 +72,7 @@ class Post < ActiveRecord::Base 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' + belongs_to :parent_post, class_name: 'Post', foreign_key: 'parent_post_id' validates :author, presence: true validates :title, length: { maximum: 35 } @@ -44,8 +80,19 @@ class Post < ActiveRecord::Base before_destroy :destroy_callback def destroy_callback - if title == "can't destroy me" - errors.add(:title, "can't destroy me") + case title + when "can't destroy me", "can't destroy me either" + errors.add(:base, "can't destroy me") + + # :nocov: + if Rails::VERSION::MAJOR >= 5 + throw(:abort) + else + return false + end + # :nocov: + when "locked title" + errors.add(:title, "is locked") # :nocov: if Rails::VERSION::MAJOR >= 5 @@ -78,6 +125,8 @@ class Firm < Company class Tag < ActiveRecord::Base has_and_belongs_to_many :posts, join_table: :posts_tags has_and_belongs_to_many :planets, join_table: :planets_tags + + has_and_belongs_to_many :comments, join_table: :comments_tags end class Section < ActiveRecord::Base @@ -102,7 +151,7 @@ class Cat < ActiveRecord::Base class IsoCurrency < ActiveRecord::Base self.primary_key = :code - # has_many :expense_entries, foreign_key: 'currency_code' + has_many :expense_entries, foreign_key: 'currency_code' end class ExpenseEntry < ActiveRecord::Base @@ -161,6 +210,7 @@ class Like < ActiveRecord::Base end class Breed + include ActiveModel::Model def initialize(id = nil, name = nil) if id.nil? @@ -179,19 +229,7 @@ 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 + validates :name, presence: true end class Book < ActiveRecord::Base @@ -199,6 +237,10 @@ class Book < ActiveRecord::Base has_many :approved_book_comments, -> { where(approved: true) }, class_name: "BookComment" has_and_belongs_to_many :authors, join_table: :book_authors, class_name: "Person" + + scope :not_banned, -> { + where(banned: false) + } end class BookComment < ActiveRecord::Base @@ -206,7 +248,7 @@ class BookComment < ActiveRecord::Base belongs_to :book def self.for_user(current_user) - records = self + records = self.all # Hide the unapproved comments from people who are not book admins unless current_user && current_user.book_admin records = records.where(approved: true) @@ -266,7 +308,13 @@ class Category < ActiveRecord::Base end class Picture < ActiveRecord::Base + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + belongs_to :imageable, polymorphic: true + belongs_to :document, -> { where( pictures: { imageable_type: 'Document' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' + belongs_to :product, -> { where( pictures: { imageable_type: 'Product' } ).eager_load( :pictures ) }, foreign_key: 'imageable_id' + + has_one :file_properties, as: 'fileable' end class Vehicle < ActiveRecord::Base @@ -281,13 +329,19 @@ class Boat < Vehicle class Document < ActiveRecord::Base has_many :pictures, as: :imageable + belongs_to :author, class_name: 'Person', foreign_key: 'author_id' + has_one :file_properties, as: 'fileable' end -class Document::Topic < Document +class Product < ActiveRecord::Base + has_many :pictures, as: :imageable + belongs_to :designer, class_name: 'Person', foreign_key: 'designer_id' + has_one :file_properties, as: 'fileable' end -class Product < ActiveRecord::Base - has_one :picture, as: :imageable +class FileProperties < ActiveRecord::Base + belongs_to :fileable, polymorphic: true + belongs_to :tag end class Make < ActiveRecord::Base @@ -313,8 +367,8 @@ class Thing < ActiveRecord::Base 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 + 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 @@ -346,6 +400,58 @@ class Customer < Customer end end +class Storage < ActiveRecord::Base + has_one :keeper, class_name: 'Keeper', as: :keepable +end + +class Keeper < ActiveRecord::Base + belongs_to :keepable, polymorphic: true +end + +class AccessCard < ActiveRecord::Base + has_many :workers +end + +class Worker < ActiveRecord::Base + belongs_to :access_card +end + +class Agency < ActiveRecord::Base +end + +class Indicator < ActiveRecord::Base + belongs_to :agency + has_many :widgets, primary_key: :import_id, foreign_key: :indicator_import_id +end + +class Widget < ActiveRecord::Base + belongs_to :indicator, primary_key: :import_id, foreign_key: :indicator_import_id +end + +class Robot < ActiveRecord::Base +end + +class Painter < ActiveRecord::Base + has_many :paintings +end + +class Painting < ActiveRecord::Base + belongs_to :painter + has_many :collectors +end + +class Collector < ActiveRecord::Base + belongs_to :painting +end + +class List < ActiveRecord::Base + has_many :items, class_name: 'ListItem', inverse_of: :list +end + +class ListItem < ActiveRecord::Base + belongs_to :list, inverse_of: :items +end + ### PORO Data - don't do this in a production app $breed_data = BreedData.new $breed_data.add(Breed.new(0, 'persian')) diff --git a/test/support/active_record/rollback.rb b/test/support/active_record/rollback.rb new file mode 100644 index 000000000..cbd06d4a8 --- /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 diff --git a/test/support/orm/active_record/setup.rb b/test/support/active_record/setup.rb similarity index 89% rename from test/support/orm/active_record/setup.rb rename to test/support/active_record/setup.rb index 1e2cb1fae..f0c94dfcb 100644 --- a/test/support/orm/active_record/setup.rb +++ b/test/support/active_record/setup.rb @@ -1,6 +1,3 @@ - -JSONAPI.configuration.default_record_accessor_klass = JSONAPI::ActiveRecordRecordAccessor - TestApp.class_eval do config.active_record.schema_format = :none diff --git a/test/support/controllers_resources_processors.rb b/test/support/controllers_resources_processors.rb index d48867809..2af166215 100644 --- a/test/support/controllers_resources_processors.rb +++ b/test/support/controllers_resources_processors.rb @@ -1,57 +1,5 @@ # Controllers, Resources, and Processors for specs. -class Storage < ActiveRecord::Base - has_one :keeper, class_name: 'Keeper', as: :keepable -end - -class Keeper < ActiveRecord::Base - belongs_to :keepable, polymorphic: true -end - -class AccessCard < ActiveRecord::Base - has_many :workers -end - -class Worker < ActiveRecord::Base - belongs_to :access_card -end - -class Agency < ActiveRecord::Base -end - -class Indicator < ActiveRecord::Base - belongs_to :agency - has_many :widgets, primary_key: :import_id, foreign_key: :indicator_import_id -end - -class Widget < ActiveRecord::Base - belongs_to :indicator, primary_key: :import_id, foreign_key: :indicator_import_id -end - -class Robot < ActiveRecord::Base -end - -class Painter < ActiveRecord::Base - has_many :paintings -end - -class Painting < ActiveRecord::Base - belongs_to :painter - has_many :collectors -end - -class Collector < ActiveRecord::Base - belongs_to :painting -end - -class List < ActiveRecord::Base - has_many :items, class_name: 'ListItem', inverse_of: :list -end - -class ListItem < ActiveRecord::Base - belongs_to :list, inverse_of: :items -end - ### CONTROLLERS class SessionsController < ActionController::Base include JSONAPI::ActsAsResourceController diff --git a/test/support/database/config.yml b/test/support/database/config.yml new file mode 100644 index 000000000..0cda30abf --- /dev/null +++ b/test/support/database/config.yml @@ -0,0 +1,5 @@ +test: + adapter: sqlite3 + database: test_db + pool: 5 + timeout: 5000 diff --git a/test/support/database/generator.rb b/test/support/database/generator.rb new file mode 100644 index 000000000..4e1b9f367 --- /dev/null +++ b/test/support/database/generator.rb @@ -0,0 +1,476 @@ +# 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 :sessions, id: false, force: true do |t| + t.string :id, :limit => 36, :primary_key => true, null: false + t.string :survey_id, :limit => 36, null: false + + t.timestamps + end + + create_table :responses, force: true do |t| + #t.string :id, :limit => 36, :primary_key => true, null: false + + t.string :session_id, limit: 36, null: false + + t.string :type + t.string :question_id, limit: 36 + + t.timestamps + end + + create_table :response_texts, force: true do |t| + t.text :text + t.integer :response_id + + t.timestamps + end + + 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 + t.timestamps null: false + 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.string :nickname + 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 :author_id + t.references :imageable, polymorphic: true, index: true + t.timestamps null: false + end + + create_table :documents, force: true do |t| + t.string :name + t.integer :author_id + t.timestamps null: false + end + + create_table :products, force: true do |t| + t.string :name + t.integer :designer_id + t.timestamps null: false + end + + create_table :file_properties, force: true do |t| + t.string :name + #t.timestamps null: false + t.references :fileable, polymorphic: true, index: true + t.belongs_to :tag, index: true + + t.integer :size + 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 + t.timestamps null: false + end + + create_table :answers, force: true do |t| + t.references :question + t.integer :respondent_id + t.string :respondent_type + t.string :text + t.timestamps null: false + end + + create_table :patients, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :doctors, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :painters, force: true do |t| + t.string :name + + t.timestamps null: false + end + + create_table :paintings, force: true do |t| + t.string :title + t.string :category + t.belongs_to :painter + + t.timestamps null: false + end + + create_table :collectors, force: true do |t| + t.string :name + t.belongs_to :painting + end + + create_table :lists, force: true do |t| + t.string :name + end + + create_table :list_items, force: true do |t| + t.belongs_to :list + end + + # special cases + create_table :storages, force: true do |t| + t.string :token, null: false + t.string :name + t.timestamps null: false + end + + create_table :keepers, force: true do |t| + t.string :name + t.string :keepable_type, null: false + t.integer :keepable_id, null: false + t.timestamps null: false + end + + create_table :access_cards, force: true do |t| + t.string :token, null: false + t.string :security_level + t.timestamps null: false + end + + create_table :workers, force: true do |t| + t.string :name + t.integer :access_card_id, null: false + t.timestamps null: false + end + + create_table :agencies, force: true do |t| + t.string :name + t.timestamps null: false + end + + create_table :indicators, force: true do |t| + t.string :name + t.string :import_id + t.integer :agency_id, null: false + t.timestamps null: false + end + + create_table :widgets, force: true do |t| + t.string :name + t.string :indicator_import_id, null: false + t.timestamps null: false + end + + create_table :robots, force: true do |t| + t.string :name + t.integer :version + t.timestamps null: false + end + 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 diff --git a/test/support/inflections.rb b/test/support/inflections.rb new file mode 100644 index 000000000..6700d9393 --- /dev/null +++ b/test/support/inflections.rb @@ -0,0 +1,7 @@ +# 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' + inflect.uncountable 'file_properties' +end 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/active_record/schema.rb b/test/support/orm/active_record/schema.rb deleted file mode 100644 index 2b76e9b11..000000000 --- a/test/support/orm/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/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/sequel/app_config.rb b/test/support/sequel/app_config.rb new file mode 100644 index 000000000..e69de29bb diff --git a/test/support/sequel/import_schema.rb b/test/support/sequel/import_schema.rb new file mode 100644 index 000000000..64542d629 --- /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 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 99% rename from test/support/orm/sequel/models.rb rename to test/support/sequel/models.rb index 2c132f740..971da42b1 100644 --- a/test/support/orm/sequel/models.rb +++ b/test/support/sequel/models.rb @@ -1,6 +1,5 @@ require 'sequel' require 'jsonapi-resources' -require_relative 'schema' config = Rails.configuration.database_configuration["test"] config["adapter"] = "sqlite" if config["adapter"]=="sqlite3" diff --git a/test/support/sequel/rollback.rb b/test/support/sequel/rollback.rb new file mode 100644 index 000000000..914855caf --- /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 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/support/orm/sequel/setup.rb b/test/support/sequel/setup.rb similarity index 76% rename from test/support/orm/sequel/setup.rb rename to test/support/sequel/setup.rb index 1ace5e634..98eb3921b 100644 --- a/test/support/orm/sequel/setup.rb +++ b/test/support/sequel/setup.rb @@ -1,6 +1,3 @@ - -JSONAPI.configuration.default_record_accessor_klass = JSONAPI::ActiveRecordRecordAccessor - TestApp.class_eval do config.active_record.schema_format = :none @@ -9,4 +6,4 @@ 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 +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8aa8fc749..80a51243a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,5 +1,5 @@ -require 'simplecov' require 'database_cleaner' +require 'simplecov' # To run tests with coverage: # COVERAGE=true bundle exec rake test @@ -23,7 +23,8 @@ ENV["ORM"] = "active_record" -require_relative "support/orm/#{ENV["ORM"]}/initialize" +require_relative "support/#{ENV["ORM"]}/initialize" +require_relative "support/inflections" require 'rails/test_help' require 'minitest/mock' require 'jsonapi-resources' @@ -56,6 +57,8 @@ class TestApp < Rails::Application #Raise errors on unsupported parameters config.action_controller.action_on_unpermitted_parameters = :raise + config.paths["config/database"] = "support/database/config.yml" + config.active_support.test_order = :random ActiveSupport::Deprecation.silenced = true @@ -219,7 +222,15 @@ def show_queries TestApp.initialize! -require_relative "support/orm/#{ENV["ORM"]}/models" +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" module Pets @@ -722,4 +733,4 @@ def unformat(formatted_route) end end -require_relative "support/orm/#{ENV["ORM"]}/setup" +require_relative "support/#{ENV["ORM"]}/setup"