├── spec
├── test_app
│ ├── log
│ │ └── .gitkeep
│ ├── app
│ │ ├── models
│ │ │ └── .gitkeep
│ │ ├── mailers
│ │ │ └── .gitkeep
│ │ ├── views
│ │ │ ├── models.rb
│ │ │ ├── home
│ │ │ │ └── index.html.erb
│ │ │ ├── components.rb
│ │ │ ├── models
│ │ │ │ ├── address.rb
│ │ │ │ ├── comment.rb
│ │ │ │ ├── todo_item.rb
│ │ │ │ └── user.rb
│ │ │ ├── layouts
│ │ │ │ └── application.html.erb
│ │ │ └── components
│ │ │ │ └── test.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── assets
│ │ │ ├── javascripts
│ │ │ │ ├── reactive_record_config.js
│ │ │ │ ├── components
│ │ │ │ │ ├── empty_component.rb
│ │ │ │ │ ├── todo_item_component.js.rb
│ │ │ │ │ ├── another_component.rb
│ │ │ │ │ ├── todos_component.js.rb
│ │ │ │ │ └── todos_main_component.rb
│ │ │ │ ├── application.rb
│ │ │ │ └── spec
│ │ │ │ │ └── reactive_record_xspec.js.rb
│ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ └── controllers
│ │ │ ├── home_controller.rb
│ │ │ ├── test_controller.rb
│ │ │ └── application_controller.rb
│ ├── public
│ │ ├── favicon.ico
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── db
│ │ ├── migrate
│ │ │ ├── 20160129182544_add_test_enum_to_user.rb
│ │ │ ├── 20150908184118_add_address_id_to_user.rb
│ │ │ ├── 20150828172008_add_single_comment_to_todo_item.rb
│ │ │ ├── 20151009000111_add_test_data_attributes_to_user.rb
│ │ │ ├── 20150826142045_create_comments.rb
│ │ │ ├── 20150617002932_create_todo_items.rb
│ │ │ ├── 20150617134028_create_users.rb
│ │ │ ├── 20150917220236_add_second_address_to_user.rb
│ │ │ └── 20150729195556_add_address_to_user.rb
│ │ ├── seeds.rb
│ │ └── schema.rb
│ ├── config
│ │ ├── environment.rb
│ │ ├── routes.rb
│ │ ├── initializers
│ │ │ ├── mime_types.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── session_store.rb
│ │ │ ├── secret_token.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ └── inflections.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── boot.rb
│ │ ├── database.yml
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ │ └── application.rb
│ ├── Rakefile
│ ├── spec-opal
│ │ ├── index.html.erb
│ │ ├── active_record
│ │ │ ├── reactive_record_load_spec.rb
│ │ │ ├── dummy_value_spec.rb
│ │ │ ├── many_to_many_spec.rb
│ │ │ ├── aggregations_spec.rb
│ │ │ ├── enum_spec.rb
│ │ │ ├── instance_methods_spec.rb
│ │ │ ├── prerendering_spec.rb
│ │ │ ├── non_ar_aggregations_spec.rb
│ │ │ ├── associations_spec.rb
│ │ │ ├── update_aggregations_spec.rb
│ │ │ ├── virtual_methods_spec.rb
│ │ │ ├── revert_record_spec.rb
│ │ │ ├── scope_spec.rb
│ │ │ ├── update_scopes_spec.rb
│ │ │ ├── base_spec.rb
│ │ │ ├── save_spec.rb
│ │ │ ├── edge_cases_spec.rb
│ │ │ ├── update_associations_spec.rb
│ │ │ ├── update_attributes_spec.rb
│ │ │ ├── permissions_spec.rb
│ │ │ └── rendering_spec.rb
│ │ ├── spec_helper.js.rb
│ │ └── vendor
│ │ │ └── es5-shim.min.js
│ ├── script
│ │ └── rails
│ ├── Gemfile
│ ├── config.ru
│ ├── spec
│ │ ├── server_unit_tests
│ │ │ └── pry_rescue_spec.rb
│ │ └── spec_helper.rb
│ ├── Gemfile.lock
│ └── README.rdoc
└── server_unit_tests
│ └── pry_rescue_spec.rb
├── .rspec
├── lib
├── reactive_record
│ ├── version.rb
│ ├── active_record
│ │ ├── base.rb
│ │ ├── error.rb
│ │ ├── aggregations.rb
│ │ ├── associations.rb
│ │ ├── instance_methods.rb
│ │ ├── class_methods.rb
│ │ └── reactive_record
│ │ │ ├── collection.rb
│ │ │ └── while_loading.rb
│ ├── serializers.rb
│ ├── pry.rb
│ ├── engine.rb
│ ├── reactive_scope.rb
│ ├── permissions.rb
│ └── interval.rb
├── json_parse_patch.rb
├── Gemfile
└── reactive-record.rb
├── .gitignore
├── app
└── controllers
│ └── reactive_record
│ ├── application_controller.rb
│ └── reactive_record_controller.rb
├── script
└── rails
├── README.md
├── config
└── routes.rb
├── path-release-steps.md
├── Gemfile
├── .codeclimate.yml
├── Rakefile
├── MIT-LICENSE
├── LICENSE
├── reactive-record.gemspec
└── Gemfile.lock
/spec/test_app/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/test_app/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/test_app/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/test_app/app/mailers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format documentation
--------------------------------------------------------------------------------
/spec/test_app/app/views/models.rb:
--------------------------------------------------------------------------------
1 | require_tree './models'
2 |
--------------------------------------------------------------------------------
/spec/test_app/app/views/home/index.html.erb:
--------------------------------------------------------------------------------
1 | Hello there check the console
--------------------------------------------------------------------------------
/lib/reactive_record/version.rb:
--------------------------------------------------------------------------------
1 | module ReactiveRecord
2 | VERSION = "0.8.3"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/test_app/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/javascripts/reactive_record_config.js:
--------------------------------------------------------------------------------
1 |
2 | window.ReactiveRecordEnginePath = "/rr"
--------------------------------------------------------------------------------
/spec/test_app/app/views/components.rb:
--------------------------------------------------------------------------------
1 | require 'opal'
2 | require 'reactive-record'
3 | require 'models'
4 | require_tree './components'
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | log/*.log
3 | pkg/
4 | spec/test_app/db/*.sqlite3
5 | spec/test_app/log/
6 | spec/test_app/tmp/
7 | spec/test_app/.sass-cache
8 | .DS_Store
--------------------------------------------------------------------------------
/app/controllers/reactive_record/application_controller.rb:
--------------------------------------------------------------------------------
1 | module ReactiveRecord
2 | class ApplicationController < ActionController::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/javascripts/components/empty_component.rb:
--------------------------------------------------------------------------------
1 | class EmptyComponent
2 | include React::Component
3 | export_component
4 | def render
5 | end
6 | end
--------------------------------------------------------------------------------
/spec/test_app/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | class HomeController < ApplicationController
2 |
3 | def index
4 | redirect_to "/opal_spec"
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/lib/reactive_record/active_record/base.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | class Base
3 |
4 | extend ClassMethods
5 |
6 | include InstanceMethods
7 |
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/javascripts/application.rb:
--------------------------------------------------------------------------------
1 | require 'opal'
2 | require 'opal_ujs'
3 | #require 'react'
4 | require 'reactrb'
5 | # not sure why we are not requiring components
6 |
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20160129182544_add_test_enum_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddTestEnumToUser < ActiveRecord::Migration
2 | def change
3 | add_column :users, :test_enum, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/test_app/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Dummy::Application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20150908184118_add_address_id_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddAddressIdToUser < ActiveRecord::Migration
2 | def change
3 | add_column :users, :address_id, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/test_app/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 |
3 | root :to => "home#index"
4 | match 'test', :to => "test#index", via: :get
5 | mount ReactiveRecord::Engine => "/rr"
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20150828172008_add_single_comment_to_todo_item.rb:
--------------------------------------------------------------------------------
1 | class AddSingleCommentToTodoItem < ActiveRecord::Migration
2 | def change
3 | add_column :todo_items, :comment_id, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/test_app/app/controllers/test_controller.rb:
--------------------------------------------------------------------------------
1 | class TestController < ApplicationController
2 |
3 | def index
4 | render inline: "<%= react_component 'Test', {}, { prerender: !params[:no_prerender] } %>", layout: nil
5 | end
6 |
7 | end
--------------------------------------------------------------------------------
/spec/test_app/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | #protect_from_forgery
3 |
4 | def acting_user
5 | cookies[:acting_user] and User.find_by_email(cookies[:acting_user])
6 | end
7 |
8 | end
9 |
--------------------------------------------------------------------------------
/spec/test_app/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/spec/test_app/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20151009000111_add_test_data_attributes_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddTestDataAttributesToUser < ActiveRecord::Migration
2 | def change
3 | add_column :users, :data_string, :string
4 | add_column :users, :data_times, :integer
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/reactive_record/serializers.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Base.send(:define_method, :react_serializer) do
2 | serializable_hash.merge(ReactiveRecord::Base.get_type_hash(self))
3 | end
4 |
5 | ActiveRecord::Relation.send(:define_method, :react_serializer) do
6 | all.to_a.react_serializer
7 | end
8 |
--------------------------------------------------------------------------------
/spec/test_app/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | if File.exist?(gemfile)
5 | ENV['BUNDLE_GEMFILE'] = gemfile
6 | require 'bundler'
7 | Bundler.setup
8 | end
9 |
10 | $:.unshift File.expand_path('../../../../lib', __FILE__)
--------------------------------------------------------------------------------
/spec/test_app/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | Dummy::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20150826142045_create_comments.rb:
--------------------------------------------------------------------------------
1 | class CreateComments < ActiveRecord::Migration
2 | def change
3 | create_table :comments do |t|
4 | t.integer :user_id
5 | t.integer :todo_item_id
6 | t.string :comment
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20150617002932_create_todo_items.rb:
--------------------------------------------------------------------------------
1 | class CreateTodoItems < ActiveRecord::Migration
2 | def change
3 | create_table :todo_items do |t|
4 | t.string :title
5 | t.text :description
6 | t.boolean :complete
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= javascript_include_tag 'es5-shim.min' %>
7 | <%= javascript_include_tag @server.main %>
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/lib/reactive_record/pry.rb:
--------------------------------------------------------------------------------
1 | module ReactiveRecord
2 |
3 | module Pry
4 |
5 | def self.rescued(e)
6 | if defined?(PryRescue) && e.instance_variable_defined?(:@rescue_bindings) && !e.is_a?(ReactiveRecord::AccessViolation)
7 | ::Pry::rescued(e)
8 | end
9 | end
10 |
11 | end
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/spec/test_app/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | ENGINE_ROOT = File.expand_path('../..', __FILE__)
5 | ENGINE_PATH = File.expand_path('../../lib/reactive_record/engine', __FILE__)
6 |
7 | require 'rails/all'
8 | require 'rails/engine/commands'
9 |
--------------------------------------------------------------------------------
/spec/test_app/app/views/models/address.rb:
--------------------------------------------------------------------------------
1 | class Address < ActiveRecord::Base
2 |
3 | MAPPED_FIELDS = %w(id street city state zip)
4 |
5 | def self.compose(*args)
6 | new.tap do |address|
7 | MAPPED_FIELDS.each_with_index do |field_name, i|
8 | address.send("#{field_name}=", args[i])
9 | end
10 | end
11 | end
12 |
13 | end
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20150617134028_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def change
3 | create_table :users do |t|
4 | t.string :first_name
5 | t.string :last_name
6 | t.string :email
7 |
8 | t.timestamps
9 | end
10 |
11 | add_column :todo_items, :user_id, :integer
12 |
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/json_parse_patch.rb:
--------------------------------------------------------------------------------
1 | begin
2 | JSON.parse("test")
3 | rescue Exception => e
4 | JSON.class_eval do
5 | class << self
6 | alias old_parse parse
7 | end
8 | def self.parse(*args, &block)
9 | old_parse *args, &block
10 | rescue Exception => e
11 | raise StandardError.new e.message
12 | end
13 | end unless e.is_a? StandardError
14 | end
15 |
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20150917220236_add_second_address_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddSecondAddressToUser < ActiveRecord::Migration
2 | def change
3 |
4 | add_column :users, :address2_street, :string
5 | add_column :users, :address2_city, :string
6 | add_column :users, :address2_state, :string
7 | add_column :users, :address2_zip, :string
8 |
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Reactive Record
2 |
3 | The `reactive-record` gem has been supeceeded by the `hyper-mesh` gem, which is largely upwards compatible but includes server side data push (via transports such as ActionCable, Pusher.com, etc) and many other improvements.
4 |
5 | Docs are here: http://ruby-hyperloop.io/docs/hypermesh_overview/
6 | Gem is here: https://github.com/ruby-hyperloop/hyper-mesh
7 |
--------------------------------------------------------------------------------
/spec/test_app/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 |
6 | <%= stylesheet_link_tag "application", :media => "all" %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 | <%= javascript_include_tag "application" unless params[:no_js] %>
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | ReactiveRecord::Engine.routes.draw do
2 | root :to => "reactive_record#fetch", via: :post
3 | match 'save', to: 'reactive_record#save', via: :post
4 | match 'destroy', to: 'reactive_record#destroy', via: :post
5 | match 'syncromesh-subscribe', to: 'syncromesh#subscribe', via: :get
6 | match 'syncromesh-read/:subscriber', to: 'syncromesh#read', via: :get
7 | end
8 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/javascripts/components/todo_item_component.js.rb:
--------------------------------------------------------------------------------
1 | require 'reactrb'
2 |
3 | class TodoItemComponent
4 |
5 | include React::Component
6 |
7 | required_param :todo
8 | backtrace :on
9 |
10 | def render
11 | div do
12 | "Title: #{todo.title}".br; "Description #{todo.description}".br; "User #{todo.user.name}"
13 | end
14 | end
15 |
16 | end
17 |
--------------------------------------------------------------------------------
/lib/reactive_record/engine.rb:
--------------------------------------------------------------------------------
1 | #require 'rails'
2 |
3 | module ReactiveRecord
4 | class Engine < ::Rails::Engine
5 | isolate_namespace ReactiveRecord
6 | config.generators do |g|
7 | g.test_framework :rspec, :fixture => false
8 | g.fixture_replacement :factory_girl, :dir => 'spec/factories'
9 | g.assets false
10 | g.helper false
11 | end
12 | end
13 | end
--------------------------------------------------------------------------------
/spec/test_app/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/spec/test_app/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # Dummy::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/path-release-steps.md:
--------------------------------------------------------------------------------
1 | git For example assuming you are releasing fix to 0.8.18
2 |
3 | 1. Checkout 0-8-stable
4 | 2. Update tests, fix the bug and commit the changes.
5 | 3. Build & Release to RubyGems (Remember the version in version.rb should already be 0.8.19)
6 | 4. Create a tag 'v0.8.19' pointing to that commit.
7 | 5. Bump the version in 0-8-stable to 0.8.20 so it will be ready for the next patch level release.
8 | 6. Commit the version bump, and do a `git push --tags` so the new tag goes up
9 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/javascripts/components/another_component.rb:
--------------------------------------------------------------------------------
1 | require 'user'
2 | class AnotherComponent
3 |
4 | include React::Component
5 |
6 | export_component
7 |
8 | required_param :user, type: User
9 |
10 | backtrace :on
11 |
12 | def render
13 | div do
14 | "#{user.name}'s todos:".br
15 | ul do
16 | broken!
17 | user.todo_items.each do |todo|
18 | li { TodoItemComponent(todo: todo) }
19 | end
20 | end
21 | end
22 | end
23 |
24 | end
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 |
2 |
3 | # gem 'reactive-ruby', File.exist?("../reactive-ruby") ? {path: "../reactive-ruby"} : "reactive-ruby"
4 | # gem 'opal' #, git: "https://github.com/catprintlabs/opal.git"
5 | # gem 'opal-browser'
6 | # gem 'react-rails', File.exist?("../react-rails") ? {path: "../react-rails"} : {git: "https://github.com/catprintlabs/react-rails.git", :branch => 'isomorphic-methods-support'}
7 | #gem 'reactrb', path: "../reactive-ruby", branch: "0-8-stable"
8 | #gem 'opal-rails', git: "https://github.com/opal/opal-rails.git"
9 | gemspec
10 |
--------------------------------------------------------------------------------
/lib/reactive_record/reactive_scope.rb:
--------------------------------------------------------------------------------
1 | class ActiveRecord::Base
2 |
3 | def self.to_sync(scope_name, opts={}, &block)
4 | watch_list = if opts[:watch]
5 | [*opts.delete[:watch]]
6 | else
7 | [self]
8 | end
9 | if RUBY_ENGINE=='opal'
10 | watch_list.each do |klass_to_watch|
11 | ReactiveRecord::Base.sync_blocks[klass_to_watch][self][scope_name] << block
12 | end
13 | else
14 | # this is where we put server side watchers in place to sync all clients!
15 | end
16 | end
17 |
18 | end
19 |
--------------------------------------------------------------------------------
/spec/test_app/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Dummy::Application.config.secret_token = '2fce5780e24de7608de5160433088226a6f908d516b8b2117e8bc7d7c25e63d2cae3b0f56f1cb40ec1b38cab2e2e6d111d0aa28609ca79f0431b9fe90294146c'
8 |
--------------------------------------------------------------------------------
/spec/test_app/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # Disable root element in JSON by default.
12 | ActiveSupport.on_load(:active_record) do
13 | self.include_root_in_json = false
14 | end
15 |
--------------------------------------------------------------------------------
/lib/reactive_record/active_record/error.rb:
--------------------------------------------------------------------------------
1 | module ActiveModel
2 |
3 | class Error
4 |
5 | attr_reader :messages
6 |
7 | def initialize(msgs = {})
8 | @messages = msgs || {}
9 | @messages.each { |attribute, messages| @messages[attribute] = messages.uniq }
10 | end
11 |
12 | def [](attribute)
13 | messages[attribute]
14 | end
15 |
16 | def delete(attribute)
17 | messages.delete(attribute)
18 | end
19 |
20 | def empty?
21 | messages.empty?
22 | end
23 |
24 | end
25 |
26 | end
--------------------------------------------------------------------------------
/spec/test_app/db/migrate/20150729195556_add_address_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddAddressToUser < ActiveRecord::Migration
2 |
3 | def change
4 |
5 | add_column :users, :address_street, :string
6 | add_column :users, :address_city, :string
7 | add_column :users, :address_state, :string
8 | add_column :users, :address_zip, :string
9 |
10 | create_table :addresses do |t|
11 | t.string :street
12 | t.string :city
13 | t.string :state
14 | t.string :zip
15 | t.timestamps
16 | end
17 |
18 | end
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/spec/test_app/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 | gem "rails"
3 | gem 'sqlite3'
4 | gem 'react-rails' #, "1.3.3"
5 | gem 'opal', '~> 0.9.0'
6 | gem 'reactrb' #, git: "https://github.com/reactrb/reactrb.git", branch: "0-8-stable" # path: "../../../reactrb"
7 | gem 'opal-rails'#, git: "https://github.com/reactrb/opal-rails.git"
8 | gem 'reactive-record', path: "../.."
9 | gem 'therubyracer'
10 | gem 'jquery-cookie-rails'
11 | gem 'byebug'
12 | gem 'pry-rails'
13 | gem 'pry-rescue'
14 | gem 'opal-rspec-rails', github: 'opal/opal-rspec-rails'
15 | #gem 'opal-rspec-rails', git: 'https://github.com/reactrb/opal-rspec-rails.git'
16 |
--------------------------------------------------------------------------------
/spec/test_app/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 | #
12 | # These inflection rules are supported but not enabled by default:
13 | # ActiveSupport::Inflector.inflections do |inflect|
14 | # inflect.acronym 'RESTful'
15 | # end
16 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require_self
12 | *= require_tree .
13 | */
14 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 | engines:
3 | brakeman:
4 | enabled: true
5 | bundler-audit:
6 | enabled: true
7 | duplication:
8 | enabled: true
9 | config:
10 | languages:
11 | - ruby
12 | - javascript
13 | - python
14 | - php
15 | fixme:
16 | enabled: true
17 | rubocop:
18 | enabled: true
19 | ratings:
20 | paths:
21 | - Gemfile.lock
22 | - "**.erb"
23 | - "**.haml"
24 | - "**.rb"
25 | - "**.rhtml"
26 | - "**.slim"
27 | - "**.inc"
28 | - "**.js"
29 | - "**.jsx"
30 | - "**.module"
31 | - "**.php"
32 | - "**.py"
33 | exclude_paths:
34 | - config/
35 | - script/
36 | - spec/
37 |
--------------------------------------------------------------------------------
/spec/test_app/app/views/models/comment.rb:
--------------------------------------------------------------------------------
1 | class Comment < ActiveRecord::Base
2 |
3 | def create_permitted?
4 | # for testing we allow anything if there is no acting_user
5 | # in the real world you would have something like this:
6 | # acting_user and (acting_user.admin? or user_is? acting_user)
7 | !acting_user or user_is? acting_user
8 | end
9 |
10 | def destroy_permitted?
11 | !acting_user or user_is? acting_user
12 | end
13 |
14 | belongs_to :user
15 | belongs_to :todo_item
16 |
17 | has_one :todo, -> {}, class_name: "TodoItem" # this is just so we can test scopes params and null belongs_to relations
18 |
19 | end
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/reactive_record_load_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "ReactiveRecord.load" do
4 |
5 | it "will not find a non-existing record" do
6 | React::IsomorphicHelpers.load_context
7 | ReactiveRecord.load do
8 | User.find_by_first_name("Jon").id
9 | end.then do |id|
10 | expect(id).to be_nil
11 | end
12 | end
13 |
14 | it "will find an existing record" do
15 | React::IsomorphicHelpers.load_context
16 | ReactiveRecord.load do
17 | User.find_by_email("todd@catprint.com").id
18 | end.then do |id|
19 | expect(id).not_to be_nil
20 | end
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/lib/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Declare your gem's dependencies in reactive_record.gemspec.
4 | # Bundler will treat runtime dependencies like base dependencies, and
5 | # development dependencies will be added by default to the :development group.
6 | gemspec
7 |
8 | # jquery-rails is used by the dummy application
9 | gem "jquery-rails"
10 |
11 | # Declare any dependencies that are still in development here instead of in
12 | # your gemspec. These might include edge Rails or gems from your path or
13 | # Git. Remember to move these dependencies to your gemspec before releasing
14 | # your gem to rubygems.org.
15 |
16 | # To use debugger
17 | # gem 'debugger'
18 |
--------------------------------------------------------------------------------
/spec/test_app/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | development:
7 | adapter: sqlite3
8 | database: db/development.sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | # Warning: The database defined as "test" will be erased and
13 | # re-generated from your development database when you run "rake".
14 | # Do not set this db to the same as development or production.
15 | test:
16 | adapter: sqlite3
17 | database: db/test.sqlite3
18 | pool: 5
19 | timeout: 5000
20 |
21 | production:
22 | adapter: sqlite3
23 | database: db/production.sqlite3
24 | pool: 5
25 | timeout: 5000
26 |
--------------------------------------------------------------------------------
/spec/test_app/app/views/components/test.rb:
--------------------------------------------------------------------------------
1 | class Test
2 |
3 | include React::Component
4 |
5 | def render
6 | user = User.find_by_email("mitch@catprint.com")
7 | div do
8 | "#{Time.now.to_s} #{user.first_name}".br
9 | "zip: #{user.address.zip}".br
10 | "todos: #{user.todo_items.collect { |todo| todo.title }.join(", ")}".br
11 | "first todo in find_string(mitch) scope: #{user.todo_items.find_string("mitch").first.title}".br
12 | "a comment was made by: #{user.todo_items.first.commenters.first.email}".br
13 | "some expensive math: #{user.expensive_math(13)}".br
14 | "and a server side method: #{user.detailed_name}".br.tap { user.first_name = "joe"; user.detailed_name! }
15 | end
16 | end
17 |
18 | end
19 |
--------------------------------------------------------------------------------
/spec/test_app/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/spec/test_app/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/spec/test_app/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/javascripts/components/todos_component.js.rb:
--------------------------------------------------------------------------------
1 | require 'reactrb'
2 | require 'user'
3 | require 'reactive-record'
4 |
5 | class TodosComponent
6 |
7 | include React::Component
8 |
9 | export_component
10 |
11 | #optional_param :initial_user_email
12 | required_param :users, type: [User]
13 | #define_state :users
14 |
15 | before_mount do
16 | # `debugger`
17 | nil
18 | #users! [User.find_by_id(1), User.find_by_id(2), User.find_by_id(3)]
19 | end
20 |
21 | after_mount do
22 | #puts "after mount"
23 | # `debugger`
24 | nil
25 | end
26 |
27 | backtrace :on
28 |
29 | after_update do
30 | #puts "after update"
31 | if user
32 | # `debugger`
33 | nil
34 | end
35 | end
36 |
37 | def render
38 | div do
39 | TodosMainComponent(users: users)
40 | end.hide_while_loading
41 | end
42 |
43 | end
44 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/dummy_value_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "dummy values" do
4 |
5 | before(:each) { React::IsomorphicHelpers.load_context }
6 |
7 | it "fetches a dummy value" do
8 | expect(User.find_by_email("mitch@catprint.com").first_name.to_s.is_a?(String)).to be_truthy
9 | end
10 |
11 | it "can convert the value to a float" do
12 | expect(User.find_by_email("mitch@catprint.com").id.to_f.is_a?(Float)).to be_truthy
13 | end
14 |
15 | it "can convert the value to an int" do
16 | expect(User.find_by_email("mitch@catprint.com").id.to_i.is_a?(Integer)).to be_truthy
17 | end
18 |
19 | it "can do math on a value" do
20 | expect(1 + User.find_by_email("mitch@catprint.com").id).to eq(1)
21 | end
22 |
23 | xit "can do string things as well" do # can't because of the way strings work in opal
24 | expect("id: "+ User.find_by_email("mitch@catprint.com").id).to eq("id: ")
25 | end
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/many_to_many_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "many to many associations" do
4 |
5 | it "it is time to count some comments" do
6 | React::IsomorphicHelpers.load_context
7 | ReactiveRecord.load do
8 | TodoItem.find_by_title("a todo for mitch").comments.count
9 | end.then do |count|
10 | expect(count).to be(1)
11 | end
12 | end
13 |
14 | it "is time to see who made the comment" do
15 | ReactiveRecord.load do
16 | TodoItem.find_by_title("a todo for mitch").comments.first.user.email
17 | end.then do |email|
18 | expect(email).to eq("adamg@catprint.com")
19 | end
20 | end
21 |
22 | it "is time to get it directly through the relationship" do
23 | ReactiveRecord.load do
24 | TodoItem.find_by_title("a todo for mitch").commenters.first.email
25 | end.then do |email|
26 | expect(email).to eq("adamg@catprint.com")
27 | end
28 | end
29 |
30 | end
31 |
--------------------------------------------------------------------------------
/spec/test_app/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | if true
4 | require ::File.expand_path('../config/environment', __FILE__)
5 | run Dummy::Application
6 |
7 | else
8 | require 'bundler'
9 | Bundler.require
10 |
11 | require "opal/rspec"
12 | require "opal-jquery"
13 |
14 | Opal.append_path File.expand_path('../spec', __FILE__)
15 |
16 | sprockets_env = Opal::RSpec::SprocketsEnvironment.new rescue nil
17 | if sprockets_env
18 | run Opal::Server.new(sprockets: sprockets_env) { |s|
19 | s.main = 'opal/rspec/sprockets_runner'
20 | sprockets_env.add_spec_paths_to_sprockets
21 | s.debug = false
22 | s.index_path = 'spec/index.html.erb'
23 | }
24 | else
25 | run Opal::Server.new { |s|
26 | s.main = 'opal/rspec/sprockets_runner'
27 | s.append_path 'spec'
28 | #s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
29 | s.debug = true
30 | s.index_path = 'spec/index.html.erb'
31 | }
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require 'bundler/setup'
3 | rescue LoadError
4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5 | end
6 |
7 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
8 | load 'rails/tasks/engine.rake'
9 | Bundler::GemHelper.install_tasks
10 | Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f }
11 | require 'rspec/core'
12 | require 'rspec/core/rake_task'
13 | desc "Run all specs in spec directory (excluding plugin specs)"
14 | RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
15 |
16 | require 'opal/rspec/rake_task'
17 | require 'bundler'
18 | Bundler.require
19 |
20 | # Add our opal/ directory to the load path
21 | #Opal.append_path File.expand_path('../lib', __FILE__)
22 |
23 | Opal::RSpec::RakeTask.new(:spec_opal) do |s|
24 | s.sprockets.paths.tap { s.sprockets.clear_paths }[0..-2].each { |path| s.sprockets.append_path path}
25 | s.main = 'sprockets_runner'
26 | s.append_path 'spec-opal'
27 | end
28 |
29 | task :default => [:spec, :spec_opal]
30 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015 YOURNAME
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 catprintlabs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/aggregations_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | #require 'active_record'
3 | #Opal::RSpec::Runner.autorun
4 |
5 | class Thing < ActiveRecord::Base
6 | end
7 |
8 | class ThingContainer < ActiveRecord::Base
9 | composed_of :thing
10 | composed_of :another_thing, :class_name => Thing
11 | end
12 |
13 |
14 | describe "ActiveRecord" do
15 | after(:each) { React::API.clear_component_class_cache }
16 |
17 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work
18 |
19 | # describe "a passing dummy test" do
20 | # it "passes" do
21 | # expect(true).to be(true)
22 | # end
23 | # end
24 |
25 | describe "Aggregation Reflection" do
26 |
27 | it "knows the aggregates class" do
28 | expect(ThingContainer.reflect_on_aggregation(:thing).klass).to eq(Thing)
29 | end
30 |
31 | it "knows the aggregates attribute" do
32 | expect(ThingContainer.reflect_on_aggregation(:thing).attribute).to eq(:thing)
33 | end
34 |
35 | it "knows all the Aggregates" do
36 | expect(ThingContainer.reflect_on_all_aggregations.count).to eq(2)
37 | end
38 |
39 | end
40 |
41 | end
42 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/javascripts/spec/reactive_record_xspec.js.rb:
--------------------------------------------------------------------------------
1 | require 'spec/spec_helper'
2 | require 'user'
3 |
4 |
5 |
6 | describe "Reactive Record" do
7 |
8 | after(:each) { React::API.clear_component_class_cache }
9 |
10 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work
11 |
12 | # describe "a passing dummy test" do
13 | # it "passes" do
14 | # expect(true).to be(true)
15 | # end
16 | # end
17 |
18 | describe "reactive_record basic api" do
19 |
20 | rendering("a simple component") do
21 | div {"hello"}
22 | end.should_immediately_generate do |component|
23 | component.html == "hello"
24 | end
25 |
26 | rendering("a find_by query") do
27 | User.find_by_email("mitch@catprint.com").email
28 | end.should_immediately_generate do
29 | html == "mitch@catprint.com"
30 | end
31 |
32 | it "should yield the same find_by result if called twice" do
33 | ar1 = User.find_by_email("mitch@catprint.com")
34 | ar2 = User.find_by_email("mitch@catprint.com")
35 | expect(ar1.equal?(ar2)).to be(true)
36 | end
37 |
38 |
39 | end
40 |
41 | end
42 |
43 |
--------------------------------------------------------------------------------
/spec/test_app/app/views/models/todo_item.rb:
--------------------------------------------------------------------------------
1 | class TodoItem < ActiveRecord::Base
2 |
3 | def view_permitted?(attribute)
4 | !acting_user or user_is? acting_user
5 | end
6 |
7 | def update_permitted?
8 | return true unless acting_user
9 | return only_changed? :comments unless user_is? acting_user
10 | true
11 | end
12 |
13 | belongs_to :user
14 | has_many :comments
15 | has_many :commenters, class_name: "User", through: :comments, source: :user
16 | belongs_to :comment # just so we can test an empty belongs_to relationship
17 |
18 | scope :find_string, ->(s) { where("title LIKE ? OR description LIKE ?", "%#{s}%", "%#{s}%") }
19 |
20 | scope :active, -> { where("title LIKE '%mitch%' OR description LIKE '%mitch%'")}
21 | to_sync :active do |scope, record|
22 | if record.title =~ /mitch/ || record.description =~ /mitch/
23 | scope << record
24 | else
25 | scope.delete(record)
26 | end
27 | end
28 |
29 | scope :important, -> { where("title LIKE '%another%' OR description LIKE '%another%'")}
30 | to_sync(:important) {title =~ /another/ || description =~ /another/ }
31 |
32 | def virtual_user_first_name
33 | user.first_name
34 | end unless RUBY_ENGINE == 'opal'
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/enum_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | use_case "reading and writting enums" do
4 |
5 | async "can change the enum and read it back" do
6 | React::IsomorphicHelpers.load_context
7 | set_acting_user "super-user"
8 | user = User.find(1)
9 | user.test_enum = :no
10 | user.save.then do
11 | React::IsomorphicHelpers.load_context
12 | ReactiveRecord.load do
13 | User.find(1).test_enum
14 | end.then do |test_enum|
15 | async { expect(test_enum).to eq(:no) }
16 | end
17 | end
18 | end
19 |
20 | async "can set it back" do
21 | React::IsomorphicHelpers.load_context
22 | set_acting_user "super-user"
23 | user = User.find(1)
24 | user.test_enum = :yes
25 | user.save.then do
26 | React::IsomorphicHelpers.load_context
27 | ReactiveRecord.load do
28 | User.find(1).test_enum
29 | end.then do |test_enum|
30 | async { expect(test_enum).to eq(:yes) }
31 | end
32 | end
33 | end
34 |
35 | it "can change it back" do
36 | user = User.find(1)
37 | user.test_enum = :yes
38 | user.save.then do |success|
39 | expect(success).to be_truthy
40 | end
41 | end
42 |
43 | end
44 |
--------------------------------------------------------------------------------
/reactive-record.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path('../lib', __FILE__)
2 |
3 | # Maintain your gem's version:
4 | require 'reactive_record/version'
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |s|
8 |
9 | s.name = 'reactive-record'
10 | s.version = ReactiveRecord::VERSION
11 | s.authors = 'Mitch VanDuyn'
12 | s.email = ['mitch@catprint.com']
13 | s.summary = %q{Access active-record models inside Reactrb components.}
14 | s.description = %q{Access active-record models inside Reactrb components. Model data is calculated during pre-rerendering, and then dynamically loaded as components update.}
15 | s.post_install_message = %q{As of version 0.8.x, reactive-record depends on reactrb instead of reactive-ruby. If you have already switched, you can ignore this message.}
16 |
17 | s.files = Dir['{app,config,db,lib}/**/*'] + ['MIT-LICENSE', 'Rakefile', 'README.md']
18 |
19 | s.test_files = Dir['spec-server/**/*']
20 |
21 | s.add_dependency 'rails', '>= 3.2.13'
22 |
23 | s.add_development_dependency 'sqlite3'
24 | s.add_development_dependency 'rspec-rails'
25 | s.add_development_dependency 'pry'
26 | s.add_development_dependency 'therubyracer'
27 |
28 | s.add_dependency 'opal-rails'
29 | s.add_dependency 'opal-browser'
30 | s.add_dependency 'react-rails'
31 |
32 | s.add_dependency 'reactrb'
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/lib/reactive-record.rb:
--------------------------------------------------------------------------------
1 | if RUBY_ENGINE == 'opal'
2 |
3 | require "reactrb"
4 | require "json_parse_patch"
5 | require "reactive_record/active_record/error"
6 | require "reactive_record/server_data_cache"
7 | require "reactive_record/active_record/reactive_record/while_loading"
8 | require "reactive_record/active_record/reactive_record/isomorphic_base"
9 | require "reactive_record/active_record/aggregations"
10 | require "reactive_record/active_record/associations"
11 | require "reactive_record/active_record/reactive_record/base"
12 | require "reactive_record/active_record/reactive_record/collection"
13 | require "reactive_record/reactive_scope"
14 | require "reactive_record/active_record/class_methods"
15 | require "reactive_record/active_record/instance_methods"
16 | require "reactive_record/active_record/base"
17 | require "reactive_record/interval"
18 |
19 | else
20 |
21 | require "opal"
22 | require "reactrb"
23 | require "reactive_record/version"
24 | require "reactive_record/permissions"
25 | require "reactive_record/engine"
26 | require "reactive_record/server_data_cache"
27 | require "reactive_record/active_record/reactive_record/isomorphic_base"
28 | require "reactive_record/reactive_scope"
29 | require "reactive_record/serializers"
30 | require "reactive_record/pry"
31 |
32 | Opal.append_path File.expand_path('../', __FILE__).untaint
33 | Opal.append_path File.expand_path('../../vendor', __FILE__).untaint
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/spec/test_app/app/assets/javascripts/components/todos_main_component.rb:
--------------------------------------------------------------------------------
1 | class TodosMainComponent
2 |
3 | include React::Component
4 |
5 | required_param :users
6 |
7 | define_state :user, :user_email
8 |
9 | before_mount do
10 | #user_email! "mitch@catprint.com"
11 | user! User.find_by_email(user_email) if user_email
12 | end
13 |
14 | def render
15 | div do
16 | if true
17 | table do
18 | tbody do
19 | tr { td {"name"}; td {"email"}; td {"number of todos"}}
20 | users.each do |user|
21 | tr {user.name.td; user.email.td; user.todo_items.count.td.while_loading("-") }
22 | end
23 | end
24 | end
25 | end
26 | div do
27 | "Todos for ".span
28 | input(type: :text, value: user_email, placeholder: "enter a user's email").
29 | on(:change) { |e| user_email! e.target.value }.
30 | on(:key_up) { |e| user! User.find_by_email(user_email) if e.key_code == 13 }
31 | end
32 | if !user
33 | "type in an email and hit return to find a user"
34 | elsif user.not_found?
35 | "#{user.email} does not exist, try another email"
36 | elsif user.todo_items.count == 0
37 | "No Todos Yet"
38 | else
39 | div do
40 | user.todo_items.each do |todo|
41 | TodoItemComponent(todo: todo)
42 | end
43 | end.while_loading "searching..."
44 | end
45 | end
46 |
47 | end
48 |
49 | end
--------------------------------------------------------------------------------
/app/controllers/reactive_record/reactive_record_controller.rb:
--------------------------------------------------------------------------------
1 | require 'reactive_record/server_data_cache'
2 |
3 | module ReactiveRecord
4 |
5 | class ReactiveRecordController < ::ApplicationController
6 |
7 | def fetch
8 | render :json => ReactiveRecord::ServerDataCache[
9 | (json_params[:models] || []).map(&:with_indifferent_access),
10 | (json_params[:associations] || []).map(&:with_indifferent_access),
11 | json_params[:pending_fetches],
12 | acting_user
13 | ]
14 | rescue Exception => e
15 | render json: {error: e.message, backtrace: e.backtrace}, status: 500
16 | end
17 |
18 | def save
19 | render :json => ReactiveRecord::Base.save_records(
20 | (json_params[:models] || []).map(&:with_indifferent_access),
21 | (json_params[:associations] || []).map(&:with_indifferent_access),
22 | acting_user,
23 | json_params[:validate],
24 | true
25 | )
26 | rescue Exception => e
27 | render json: {error: e.message, backtrace: e.backtrace}, status: 500
28 | end
29 |
30 | def destroy
31 | render :json => ReactiveRecord::Base.destroy_record(
32 | json_params[:model],
33 | json_params[:id],
34 | json_params[:vector],
35 | acting_user
36 | )
37 | rescue Exception => e
38 | render json: {error: e.message, backtrace: e.backtrace}, status: 500
39 | end
40 |
41 | private
42 |
43 | def json_params
44 | JSON.parse(params[:json]).symbolize_keys
45 | end
46 |
47 | end
48 |
49 | end
50 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/instance_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | #Opal::RSpec::Runner.autorun
3 | class Thing < ActiveRecord::Base
4 | end
5 |
6 | describe "ActiveRecord" do
7 | before(:each) { React::IsomorphicHelpers.load_context }
8 | let(:instance) { Thing.new({attr1: 1, attr2: 2, id: 123}) }
9 | after(:each) { React::API.clear_component_class_cache }
10 |
11 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work
12 |
13 | # describe "a passing dummy test" do
14 | # it "passes" do
15 | # expect(true).to be(true)
16 | # end
17 | # end
18 |
19 | describe "Instance Methods" do
20 |
21 | it "will have the attributes loaded" do
22 | expect(instance.attr1).to eq(1)
23 | end
24 |
25 | it "will not have a primary key if loaded from a hash" do
26 | expect(instance.id).to be(nil)
27 | end
28 |
29 | it "reports being changed if new" do
30 | expect(instance.changed?).to be_truthy
31 | end
32 |
33 | it "reports not being changed if loaded from db" do
34 | expect(Thing.find(123).changed?).to be_falsy
35 | end
36 |
37 | it "reports being changed, if I do change it" do
38 | Thing.find(1234).my_attribute = "new"
39 | expect(Thing.find(1234).changed?).to be_truthy
40 | end
41 |
42 | it "does not think things are destroyed" do
43 | expect(instance).not_to be_destroyed
44 | end
45 |
46 | it "can destroy things" do
47 | instance.destroy
48 | expect(instance).to be_destroyed
49 | end
50 |
51 | end
52 |
53 | end
54 |
--------------------------------------------------------------------------------
/spec/server_unit_tests/pry_rescue_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "ServerDataCache" do
4 |
5 | before(:all) do
6 | @current_pry_definition = Object.const_get("Pry") if defined? Pry
7 | @current_pry_rescue_definition = Object.const_get("PryRescue") if defined? PryRescue
8 | end
9 |
10 | after(:all) do
11 | Object.const_set("Pry", @current_pry_definition) if @current_pry_definition
12 | Object.const_set("PryRescue", @current_pry_definition) if @current_pry_rescue_definition
13 | end
14 |
15 | it "behaves normally if there is no pry rescue" do
16 | binding.pry
17 | expect(ReactiveRecord::ServerDataCache[[],[], ["User", ["new", 10852], "fake_attribute"], nil]).to raise_error
18 | end
19 |
20 | context "will use pry rescue if it is defined" do
21 |
22 | before(:all) do
23 | dummy_pry = Class.new do
24 | def self.rescue
25 | yield
26 | end
27 | def self.rescued(e)
28 | @last_exception = e
29 | end
30 | def self.last_exception
31 | @last_exception
32 | end
33 | end
34 | Object.const_set("PryRescue", true)
35 | Object.const_set("Pry", dummy_pry)
36 | end
37 |
38 | it "and it will still raise an error" do
39 | expect(ReactiveRecord::ServerDataCache[[],[], ["User", ["new", 10852], "fake_attribute"], nil]).to raise_error
40 | end
41 |
42 | it "and it will call Pry.rescued" do
43 | ReactiveRecord::ServerDataCache[[],[], ["User", ["new", 10852], "fake_attribute"], nil] rescue nil
44 | expect(dummy_pry.last_exception).to be_a(Exception)
45 | end
46 |
47 | end
48 |
49 | end
50 |
--------------------------------------------------------------------------------
/spec/test_app/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger
20 | config.active_support.deprecation = :log
21 |
22 | # Only use best-standards-support built into browsers
23 | config.action_dispatch.best_standards_support = :builtin
24 |
25 | # Raise exception on mass assignment protection for Active Record models
26 | #config.active_record.mass_assignment_sanitizer = :strict
27 |
28 | # Log the query plan for queries taking more than this (works
29 | # with SQLite, MySQL, and PostgreSQL)
30 | #config.active_record.auto_explain_threshold_in_seconds = 0.5
31 |
32 | # Do not compress assets
33 | config.assets.compress = false
34 |
35 | # Expands the lines which load the assets
36 | config.assets.debug = false #true
37 |
38 | config.eager_load = false
39 |
40 | config.react.variant = :development
41 | config.react.addons = true
42 | end
43 |
--------------------------------------------------------------------------------
/spec/test_app/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Configure static asset server for tests with Cache-Control for performance
11 | config.serve_static_assets = true
12 | config.static_cache_control = "public, max-age=3600"
13 |
14 | # Log error messages when you accidentally call methods on nil
15 | config.whiny_nils = true
16 |
17 | # Show full error reports and disable caching
18 | config.consider_all_requests_local = true
19 | config.action_controller.perform_caching = false
20 |
21 | # Raise exceptions instead of rendering exception templates
22 | config.action_dispatch.show_exceptions = false
23 |
24 | # Disable request forgery protection in test environment
25 | config.action_controller.allow_forgery_protection = false
26 |
27 | # Tell Action Mailer not to deliver emails to the real world.
28 | # The :test delivery method accumulates sent emails in the
29 | # ActionMailer::Base.deliveries array.
30 | config.action_mailer.delivery_method = :test
31 |
32 | # Raise exception on mass assignment protection for Active Record models
33 | #config.active_record.mass_assignment_sanitizer = :strict
34 |
35 | # Print deprecation notices to the stderr
36 | config.active_support.deprecation = :stderr
37 | end
38 |
--------------------------------------------------------------------------------
/spec/test_app/spec/server_unit_tests/pry_rescue_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "ServerDataCache" do
4 |
5 | before(:all) do
6 | @current_pry_definition = Object.const_get("Pry") if defined? Pry
7 | @current_pry_rescue_definition = Object.const_get("PryRescue") if defined? PryRescue
8 | end
9 |
10 | after(:all) do
11 | Object.const_set("Pry", @current_pry_definition) if @current_pry_definition
12 | Object.const_set("PryRescue", @current_pry_definition) if @current_pry_rescue_definition
13 | end
14 |
15 | it "behaves normally if there is no pry rescue" do
16 | expect { ReactiveRecord::ServerDataCache[[],[], [["User", ["find", 1], "fake_attribute"]], nil] }.to raise_error(ActiveRecord::RecordNotFound)
17 | end
18 |
19 | context "will use pry rescue if it is defined" do
20 |
21 | before(:all) do
22 | pry = Class.new do
23 | def self.rescue
24 | yield
25 | end
26 | def self.rescued(e)
27 | @last_exception = e
28 | end
29 | def self.last_exception
30 | @last_exception
31 | end
32 | end
33 | Object.const_set("PryRescue", true)
34 | Object.const_set("Pry", pry)
35 | end
36 |
37 | it "and it will still raise an error" do
38 | expect { ReactiveRecord::ServerDataCache[[],[], [["User", ["find", 1], "fake_attribute"]], nil] }.to raise_error(ActiveRecord::RecordNotFound)
39 | end
40 |
41 | it "but it will call Pry.rescued first" do
42 | ReactiveRecord::ServerDataCache[[],[], ["User", ["new", 10852], "fake_attribute"], nil] rescue nil
43 | expect(Pry.last_exception).to be_a(Exception)
44 | end
45 |
46 | end
47 |
48 | end
49 |
--------------------------------------------------------------------------------
/lib/reactive_record/active_record/aggregations.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 |
3 | class Base
4 |
5 | def self.reflect_on_all_aggregations
6 | base_class.instance_eval { @aggregations ||= [] }
7 | end
8 |
9 | def self.reflect_on_aggregation(attribute)
10 | reflect_on_all_aggregations.detect { |aggregation| aggregation.attribute == attribute }
11 | end
12 |
13 | end
14 |
15 | module Aggregations
16 |
17 | class AggregationReflection
18 |
19 | attr_reader :klass_name
20 | attr_reader :attribute
21 | attr_reader :mapped_attributes
22 | attr_reader :constructor
23 |
24 | def construct(args)
25 |
26 | end
27 |
28 | def initialize(owner_class, macro, name, options = {})
29 | owner_class.reflect_on_all_aggregations << self
30 | @owner_class = owner_class
31 | @constructor = options[:constructor] || :new
32 | @klass_name = options[:class_name] || name.camelize
33 | @attribute = name
34 | if options[:mapping].respond_to? :collect
35 | @mapped_attributes = options[:mapping].collect &:last
36 | else
37 | ReactiveRecord::Base.log("improper aggregate definition #{@owner_class}, :#{name}, class_name: #{@klass_name} - missing mapping", :error)
38 | @mapped_attributes = []
39 | end
40 | end
41 |
42 | def klass
43 | @klass ||= Object.const_get(@klass_name)
44 | end
45 |
46 | def serialize(object)
47 | if object.nil?
48 | object # return dummy value if that is what we got
49 | else
50 | @mapped_attributes.collect { |attr| object.send(attr) }
51 | end
52 | end
53 |
54 | def deserialize(array)
55 | if array.nil?
56 | array # return dummy value if that is what we got
57 | elsif @constructor.respond_to?(:call)
58 | @constructor.call(*array)
59 | else
60 | klass.send(@constructor, *array)
61 | end
62 | end
63 |
64 | end
65 |
66 | end
67 |
68 |
69 | end
70 |
--------------------------------------------------------------------------------
/spec/test_app/db/seeds.rb:
--------------------------------------------------------------------------------
1 | users = [
2 | ["Mitch", "VanDuyn", "mitch@catprint.com"],
3 | ["Todd", "Russell", "todd@catprint.com"],
4 | ["Adam", "George", "adamg@catprint.com"],
5 | ["Test1", "Test1", "test1@catprint.com"]
6 | ]
7 |
8 | users.each do |first_name, last_name, email|
9 | User.create({
10 | first_name: first_name, last_name: last_name, email: email,
11 | address_street: "4348 Culver Road", address_city: "Rochester", address_state: "NY", address_zip: "14617"
12 | }
13 | #without_protection: true
14 | )
15 | end
16 |
17 | todo_items = [
18 | {
19 | title: "a todo for mitch",
20 | description: "mitch has a big fat todo to do!",
21 | user: User.find_by_email("mitch@catprint.com"),
22 | comments: [{user: User.find_by_email("adamg@catprint.com"), comment: "get it done mitch"}]
23 | },
24 | {
25 | title: "another todo for mitch",
26 | description: "mitch has too many todos",
27 | user: User.find_by_email("mitch@catprint.com")
28 | },
29 | {
30 | title: "do it again Todd",
31 | description: "Todd please do that great thing you did again",
32 | user: User.find_by_email("todd@catprint.com")
33 | },
34 | {
35 | title: "no user todo",
36 | description: "the description"
37 | },
38 | {
39 | title: "test 1 todo 1", description: "test 1 todo 1", user: User.find_by_email("test1@catprint.com"),
40 | comments: [
41 | {user: User.find_by_email("mitch@catprint.com"), comment: "test 1 todo 1 comment 1"},
42 | {user: User.find_by_email("mitch@catprint.com"), comment: "test 1 todo 1 comment 2"}
43 | ]
44 | },
45 | {
46 | title: "test 1 todo 2", description: "test 1 todo 2", user: User.find_by_email("test1@catprint.com"),
47 | comments: [
48 | {user: User.find_by_email("mitch@catprint.com"), comment: "test 1 todo 2 comment 1"},
49 | {user: User.find_by_email("mitch@catprint.com"), comment: "test 1 todo 2 comment 2"}
50 | ]
51 | }
52 | ]
53 |
54 | todo_items.each do |attributes|
55 | comments = attributes.delete(:comments) || []
56 | todo = TodoItem.create(attributes) #, without_protection: true)
57 | comments.each do |attributes|
58 | Comment.create(attributes.merge(todo_item: todo))
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/test_app/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20160129182544) do
15 |
16 | create_table "addresses", force: :cascade do |t|
17 | t.string "street"
18 | t.string "city"
19 | t.string "state"
20 | t.string "zip"
21 | t.datetime "created_at"
22 | t.datetime "updated_at"
23 | end
24 |
25 | create_table "comments", force: :cascade do |t|
26 | t.integer "user_id"
27 | t.integer "todo_item_id"
28 | t.string "comment"
29 | t.datetime "created_at"
30 | t.datetime "updated_at"
31 | end
32 |
33 | create_table "todo_items", force: :cascade do |t|
34 | t.string "title"
35 | t.text "description"
36 | t.boolean "complete"
37 | t.datetime "created_at"
38 | t.datetime "updated_at"
39 | t.integer "user_id"
40 | t.integer "comment_id"
41 | end
42 |
43 | create_table "users", force: :cascade do |t|
44 | t.string "first_name"
45 | t.string "last_name"
46 | t.string "email"
47 | t.datetime "created_at"
48 | t.datetime "updated_at"
49 | t.string "address_street"
50 | t.string "address_city"
51 | t.string "address_state"
52 | t.string "address_zip"
53 | t.integer "address_id"
54 | t.string "address2_street"
55 | t.string "address2_city"
56 | t.string "address2_state"
57 | t.string "address2_zip"
58 | t.string "data_string"
59 | t.integer "data_times"
60 | t.integer "test_enum"
61 | end
62 |
63 | end
64 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/prerendering_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'components/test'
3 |
4 | describe "prerendering" do
5 |
6 | it "will not return an id before preloading" do
7 | React::IsomorphicHelpers.load_context
8 | expect(User.find_by_email("mitch@catprint.com").id).not_to eq(1)
9 | end
10 |
11 | async "preloaded the records" do
12 | `window.ClientSidePrerenderDataInterface.ReactiveRecordInitialData = undefined` rescue nil
13 | container = Element[Document.body].append('').children.last
14 | complete = lambda do
15 | React::IsomorphicHelpers.load_context
16 | async do
17 | mitch = User.find_by_email("mitch@catprint.com")
18 | expect(mitch.id).to eq(1)
19 | expect(mitch.first_name).to eq("Mitch")
20 | expect(mitch.todo_items.first.title).to eq("a todo for mitch")
21 | expect(mitch.address.zip).to eq("14617")
22 | expect(mitch.todo_items.find_string("mitch").first.title).to eq("a todo for mitch")
23 | expect(mitch.todo_items.first.commenters.first.email).to eq("adamg@catprint.com")
24 | expect(mitch.expensive_math(13)).to eq(169)
25 | expect(mitch.detailed_name).to eq("M. VanDuyn - mitch@catprint.com (2 todos)")
26 | # clear out everything before moving on otherwise the initial data screws up the next test
27 | `delete window.ReactiveRecordInitialData`
28 | React::IsomorphicHelpers.load_context
29 | end
30 | end
31 | `container.load(#{"/test?ts=#{Time.now.to_i}"}, complete)`
32 | end
33 |
34 | async "does not preload everything" do
35 | `window.ClientSidePrerenderDataInterface.ReactiveRecordInitialData = undefined` rescue nil
36 | container = Element[Document.body].append('').children.last
37 | complete = lambda do
38 | React::IsomorphicHelpers.load_context
39 | async do
40 | expect(User.find_by_email("mitch@catprint.com").last_name.to_s).to eq("")
41 | # clear out everything before moving on otherwise there will be a pending load that screw up the next test
42 | `delete window.ReactiveRecordInitialData`
43 | React::IsomorphicHelpers.load_context
44 | end
45 | end
46 | `container.load(#{"/test?ts=#{Time.now.to_i}"}, complete)`
47 | end
48 |
49 | end
50 |
--------------------------------------------------------------------------------
/lib/reactive_record/active_record/associations.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 |
3 | class Base
4 |
5 | def self.reflect_on_all_associations
6 | base_class.instance_eval { @associations ||= superclass.instance_eval { (@associations && @associations.dup) || [] } }
7 | end
8 |
9 | def self.reflect_on_association(attribute)
10 | if found = reflect_on_all_associations.detect { |association| association.attribute == attribute and association.owner_class == self }
11 | found
12 | elsif superclass == Base
13 | nil
14 | else
15 | superclass.reflect_on_association(attribute)
16 | end
17 | end
18 |
19 | end
20 |
21 | module Associations
22 |
23 | class AssociationReflection
24 |
25 | attr_reader :association_foreign_key
26 | attr_reader :attribute
27 | attr_reader :macro
28 | attr_reader :owner_class
29 |
30 | def initialize(owner_class, macro, name, options = {})
31 | owner_class.reflect_on_all_associations << self
32 | @owner_class = owner_class
33 | @macro = macro
34 | @options = options
35 | @klass_name = options[:class_name] || (collection? && name.camelize.gsub(/s$/,"")) || name.camelize
36 | if @klass_name < ActiveRecord::Base
37 | @klass = @klass_name
38 | @klass_name = @klass_name.name
39 | end rescue nil
40 | @association_foreign_key = options[:foreign_key] || (macro == :belongs_to && "#{name}_id") || "#{@owner_class.name.underscore}_id"
41 | @attribute = name
42 | end
43 |
44 | def inverse_of
45 | unless @options[:through] or @inverse_of
46 | inverse_association = klass.reflect_on_all_associations.detect do | association |
47 | association.association_foreign_key == @association_foreign_key and association.klass == @owner_class and association.attribute != attribute and klass == association.owner_class
48 | end
49 | raise "Association #{@owner_class}.#{attribute} (foreign_key: #{@association_foreign_key}) has no inverse in #{@klass_name}" unless inverse_association
50 | @inverse_of = inverse_association.attribute
51 | end
52 | @inverse_of
53 | end
54 |
55 | def klass
56 | @klass ||= Object.const_get(@klass_name)
57 | end
58 |
59 | def collection?
60 | [:has_many].include? @macro
61 | end
62 |
63 | end
64 |
65 | end
66 |
67 |
68 | end
69 |
--------------------------------------------------------------------------------
/spec/test_app/app/views/models/user.rb:
--------------------------------------------------------------------------------
1 | class TestData
2 |
3 | def initialize(string, times)
4 | @string = string
5 | @times = times
6 | end
7 |
8 | attr_accessor :string
9 | attr_accessor :times
10 |
11 | def big_string
12 | puts "calling big_string #{string} * #{times}"
13 | string * times
14 | end
15 |
16 | end
17 |
18 | class User < ActiveRecord::Base
19 |
20 | def view_permitted?(attribute)
21 | return self == acting_user if acting_user
22 | super # we call super to test if its there (just for the spec) not really the right way to do it, see comments or todo_items
23 | end
24 |
25 | has_many :todo_items
26 | has_many :comments
27 | has_many :commented_on_items, class_name: "TodoItem", through: :comments, source: :todo_item
28 |
29 | composed_of :address, :class_name => 'Address', :constructor => :compose, :mapping => Address::MAPPED_FIELDS.map {|f| ["address_#{f}", f] }
30 | composed_of :address2, :class_name => 'Address', :constructor => :compose, :mapping => Address::MAPPED_FIELDS.map {|f| ["address2_#{f}", f] }
31 |
32 | composed_of :data, :class_name => 'TestData', :allow_nil => true, :mapping => [['data_string', 'string'], ['data_times', 'times']]
33 |
34 | enum test_enum: [:yes, :no]
35 |
36 | def name
37 | "#{first_name} #{last_name}"
38 | end
39 |
40 | # two examples of server side calculated attributes. The second takes a parameter.
41 | # the first does not rely on an id, so can be used before the record is saved.
42 |
43 | def detailed_name
44 | s = "#{first_name[0]}. #{last_name}" rescue ""
45 | s += " - #{email}" if email
46 | s += " (#{todo_items.size} todo#{'s' if todo_items.size > 1})" if todo_items.size > 0
47 | s
48 | end unless RUBY_ENGINE == 'opal'
49 |
50 | def expensive_math(n)
51 | n*n
52 | end unless RUBY_ENGINE == 'opal'
53 |
54 | # this is also used for remote calculation in the aggregate test
55 |
56 | def verify_zip
57 | if address.zip =~ /^\d{5}$/
58 | address.zip
59 | end
60 | end unless RUBY_ENGINE == 'opal'
61 |
62 | end
63 |
64 | class User < ActiveRecord::Base
65 |
66 | def as_json(*args)
67 | {name: "bozo"}
68 | end
69 |
70 | validates :email, format: {with: /\@.+\./}, :allow_nil => true
71 |
72 | def name=(val) # this is here to test ability to save changes to this type of psuedo attribute
73 | val = val.split(" ")
74 | self.first_name = val[0]
75 | self.last_name = val[1]
76 | end
77 |
78 | end unless RUBY_ENGINE == 'opal'
79 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/non_ar_aggregations_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "using non-ar aggregations" do
4 |
5 | it "is time to create a new user and add some data to it" do
6 | React::IsomorphicHelpers.load_context
7 | expect(User.new(first_name: "Data", data: TestData.new("hello", 3)).data.big_string).to eq("hellohellohello")
8 | end
9 |
10 | async "can be saved and restored" do
11 | User.find_by_first_name("Data").save.then do
12 | React::IsomorphicHelpers.load_context
13 | ReactiveRecord.load do
14 | User.find_by_first_name("Data").data
15 | end.then do |data|
16 | async { expect(data.big_string).to eq("hellohellohello") }
17 | end
18 | end
19 | end
20 |
21 | async "is time to change it, and force the save" do
22 | user = User.find_by_first_name("Data")
23 | user.data.string = "goodby"
24 | user.save(force: true).then do
25 | React::IsomorphicHelpers.load_context
26 | ReactiveRecord.load do
27 | User.find_by_first_name("Data").data
28 | end.then do |data|
29 | async { expect(data.big_string).to eq("goodbygoodbygoodby") }
30 | end
31 | end
32 | end
33 |
34 | async "is time to change the value completely and save it (no force needed)" do
35 | user = User.find_by_first_name("Data")
36 | user.data = TestData.new("the end", 1)
37 | user.save.then do
38 | React::IsomorphicHelpers.load_context
39 | ReactiveRecord.load do
40 | User.find_by_first_name("Data").data
41 | end.then do |data|
42 | async { expect(data.big_string).to eq("the end") }
43 | end
44 | end
45 | end
46 |
47 | async "is time to delete the value and see if returns nil after saving" do
48 | user = User.find_by_first_name("Data")
49 | user.data = nil
50 | user.save.then do
51 | React::IsomorphicHelpers.load_context
52 | ReactiveRecord.load do
53 | User.find_by_first_name("Data").data
54 | end.then do |data|
55 | async { expect(data).to be_nil }
56 | end
57 | end
58 | end
59 |
60 | it "is time to delete our user" do
61 | User.find_by_first_name("Data").destroy.then do
62 | expect(User.find_by_first_name("Data")).to be_destroyed
63 | end
64 | end
65 |
66 | it "is time to see to make sure a nil aggregate that has never had a value returns nil" do
67 | ReactiveRecord.load do
68 | User.find_by_email("mitch@catprint.com").data
69 | end.then do |data|
70 | expect(data).to be_nil
71 | end
72 | end
73 |
74 | end
75 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/associations_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | #require 'user'
3 | #require 'todo_item'
4 |
5 |
6 | class Thing < ActiveRecord::Base
7 | belongs_to :bucket
8 | end
9 |
10 | class Bucket < ActiveRecord::Base
11 | has_many :things
12 | end
13 |
14 | class OtherThing < ActiveRecord::Base
15 | has_many :things, through: :thing_group
16 | end
17 |
18 | describe "ActiveRecord" do
19 |
20 | after(:each) { React::API.clear_component_class_cache }
21 |
22 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work
23 |
24 | # describe "a passing dummy test" do
25 | # it "passes" do
26 | # expect(true).to be(true)
27 | # end
28 | # end
29 |
30 |
31 | describe "Association Reflection" do
32 |
33 | it "knows the foreign key of a belongs_to relationship" do
34 | expect(Thing.reflect_on_association(:bucket).association_foreign_key).to eq(:bucket_id)
35 | end
36 |
37 | it "knows the foreign key of a has_many relationship" do
38 | expect(Bucket.reflect_on_association(:things).association_foreign_key).to eq(:bucket_id)
39 | end
40 |
41 | it "knows the attribute name" do
42 | expect(Bucket.reflect_on_association(:things).attribute).to eq(:things)
43 | end
44 |
45 | it "knows the associated klass" do
46 | expect(Bucket.reflect_on_association(:things).klass).to eq(Thing)
47 | end
48 |
49 | it "knows the macro" do
50 | expect(Bucket.reflect_on_association(:things).macro).to eq(:has_many)
51 | end
52 |
53 | it "knows the inverse" do
54 | expect(Bucket.reflect_on_association(:things).inverse_of).to eq(:bucket)
55 | end
56 |
57 | it "knows if the association is a collection" do
58 | expect(Bucket.reflect_on_association(:things).collection?).to be_truthy
59 | end
60 |
61 | it "knows if the association is not a collection" do
62 | expect(Thing.reflect_on_association(:bucket).collection?).to be_falsy
63 | end
64 |
65 | it "knows the associated klass of a has_many_through relationship" do
66 | expect(OtherThing.reflect_on_association(:things).klass).to eq(Thing)
67 | end
68 |
69 | it "knows a has_many_through is a collection" do
70 | expect(OtherThing.reflect_on_association(:things).collection?).to be_truthy
71 | end
72 |
73 | it "does not return a inverse for a has_many_through collection" do
74 | expect(OtherThing.reflect_on_association(:things).inverse_of).to be_nil
75 | end
76 |
77 | end
78 |
79 | end
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/update_aggregations_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "updating aggregations" do
4 |
5 | before(:all) do
6 | React::IsomorphicHelpers.load_context
7 | User.new({first_name: "Jon", last_name: "Weaver"})
8 | end
9 |
10 | it "a hash can be assigned to initialize an aggregate" do
11 | expect(User.new(address: Address.new(zip:12345)).address.zip).to eq(12345)
12 | end
13 |
14 | it "a new model will have a blank aggregate" do
15 | expect(User.find_by_first_name("Jon").address.attributes[:zip]).to be_blank
16 | end
17 |
18 | it "an aggregate can be updated through the parent model" do
19 | User.find_by_first_name("Jon").address.zip = "14609"
20 | expect(User.find_by_first_name("Jon").address.zip).to eq("14609")
21 | end
22 |
23 | async "saving a model, saves the aggregate values" do
24 | User.find_by_first_name("Jon").save.then do
25 | React::IsomorphicHelpers.load_context
26 | ReactiveRecord.load do
27 | User.find_by_first_name("Jon").address.zip
28 | end.then do |zip|
29 | async { expect(zip).to eq("14609") }
30 | end
31 | end
32 | end
33 |
34 | it "an aggregate can be assigned" do
35 | user = User.find_by_first_name("Jon")
36 | user.address = Address.new({zip: "14622", city: "Rochester"})
37 | expect([user.address.zip, user.address.city]).to eq(["14622", "Rochester"])
38 | end
39 |
40 | async "and saving the model will save the address" do
41 | User.find_by_first_name("Jon").save.then do
42 | React::IsomorphicHelpers.load_context
43 | ReactiveRecord.load do
44 | [User.find_by_first_name("Jon").address.zip, User.find_by_first_name("Jon").address.city]
45 | end.then do |zip_and_city|
46 | async { expect(zip_and_city).to eq(["14622", "Rochester"]) }
47 | end
48 | end
49 | end
50 |
51 | it "two aggregates of the same type don't not get mixed up" do
52 | ReactiveRecord.load do
53 | User.find_by_first_name("Jon").address2.zip
54 | end.then do |zip|
55 | expect(zip).to be_nil
56 | end
57 | end
58 |
59 | async "can assign a model to an aggregate attribute" do
60 | address = Address.new({zip: "14622", city: "Rochester"})
61 | address.save do
62 | user = User.new
63 | user.address = address
64 | ReactiveRecord.load do
65 | user.verify_zip
66 | end.then do |value|
67 | async { expect(value).to eq("14622") }
68 | end
69 | end
70 | end
71 |
72 | after(:all) do
73 | Promise.when(Address.all.last.destroy, User.find_by_first_name("Jon").destroy)
74 | end
75 |
76 | end
77 |
--------------------------------------------------------------------------------
/spec/test_app/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = false
13 |
14 | # Compress JavaScripts and CSS
15 | config.assets.compress = true
16 |
17 | # Don't fallback to assets pipeline if a precompiled asset is missed
18 | config.assets.compile = false
19 |
20 | # Generate digests for assets URLs
21 | config.assets.digest = true
22 |
23 | # Defaults to nil and saved in location specified by config.assets.prefix
24 | # config.assets.manifest = YOUR_PATH
25 |
26 | # Specifies the header that your server uses for sending files
27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | # config.force_ssl = true
32 |
33 | # See everything in the log (default is :info)
34 | # config.log_level = :debug
35 |
36 | # Prepend all log lines with the following tags
37 | # config.log_tags = [ :subdomain, :uuid ]
38 |
39 | # Use a different logger for distributed setups
40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
41 |
42 | # Use a different cache store in production
43 | # config.cache_store = :mem_cache_store
44 |
45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
46 | # config.action_controller.asset_host = "http://assets.example.com"
47 |
48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
49 | # config.assets.precompile += %w( search.js )
50 |
51 | # Disable delivery errors, bad email addresses will be ignored
52 | # config.action_mailer.raise_delivery_errors = false
53 |
54 | # Enable threaded mode
55 | # config.threadsafe!
56 |
57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
58 | # the I18n.default_locale when a translation can not be found)
59 | config.i18n.fallbacks = true
60 |
61 | # Send deprecation notices to registered listeners
62 | config.active_support.deprecation = :notify
63 |
64 | # Log the query plan for queries taking more than this (works
65 | # with SQLite, MySQL, and PostgreSQL)
66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5
67 | end
68 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/virtual_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "virtual attributes" do
4 |
5 | it "can call a virtual method on the server" do
6 | React::IsomorphicHelpers.load_context
7 | ReactiveRecord.load do
8 | User.find(1).expensive_math(5)
9 | end.then { |virtual_answer| expect(virtual_answer).to eq(25) }
10 | end
11 |
12 | it "can call a virtual method on a new model on the server" do
13 | React::IsomorphicHelpers.load_context
14 | new_user = User.new
15 | ReactiveRecord.load do
16 | new_user.expensive_math(4)
17 | end.then { |virtual_answer| expect(virtual_answer).to eq(16) }
18 | end
19 |
20 | it "can call a simple virtual method on a new model on the server" do
21 | React::IsomorphicHelpers.load_context
22 | new_user = User.new
23 | ReactiveRecord.load do
24 | new_user.detailed_name
25 | end.then { |virtual_answer| expect(virtual_answer).to eq("") }
26 | end
27 |
28 | it "can call a simple virtual method on a new model on the server with data" do
29 | React::IsomorphicHelpers.load_context
30 | new_user = User.new
31 | new_user.first_name = "Joe"
32 | new_user.last_name = "Schmoe"
33 | ReactiveRecord.load do
34 | new_user.detailed_name
35 | end.then { |virtual_answer| expect(virtual_answer).to eq("J. Schmoe") }
36 | end
37 |
38 | it "can call a simple virtual method on an existing updated model on the server" do
39 | React::IsomorphicHelpers.load_context
40 | user = User.find(1)
41 | user.first_name = "Joe"
42 | user.last_name = "Schmoe"
43 | ReactiveRecord.load do
44 | user.detailed_name
45 | end.then { |virtual_answer| expect(virtual_answer).to eq("J. Schmoe - mitch@catprint.com (2 todos)") }
46 | end
47 |
48 | it "can call a simple virtual method involving an existing record and a new record" do
49 | React::IsomorphicHelpers.load_context
50 | new_record = TodoItem.new
51 | ReactiveRecord.load do
52 | existing_record = User.find("1")
53 | new_record.user = existing_record
54 | new_record.virtual_user_first_name
55 | end.then { |virtual_answer| expect(virtual_answer).to eq("Mitch") }
56 | end
57 |
58 | it "can call a simple virtual method on a new model on the server with data and an updated association" do
59 | React::IsomorphicHelpers.load_context
60 | new_user = User.new
61 | new_user.first_name = "Joe"
62 | new_user.last_name = "Schmoe"
63 | todo_item = TodoItem.new
64 | todo_item.title = "Mongo DB"
65 | new_user.todo_items << TodoItem.new #todo_item
66 | ReactiveRecord.load do
67 | new_user.detailed_name
68 | end.then { |virtual_answer| expect(virtual_answer).to eq("J. Schmoe (1 todo)") }
69 | end
70 |
71 | end
72 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/revert_record_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | #require 'user'
3 | #require 'todo_item'
4 |
5 | describe "reverting records" do
6 |
7 | it "finds that the user Adam has not changed yet" do
8 | React::IsomorphicHelpers.load_context
9 | ReactiveRecord.load do
10 | User.find_by_first_name("Adam")
11 | end.then do |user|
12 | expect(user).not_to be_changed
13 | end
14 | end
15 |
16 | it "creates a new todo which should be changed (because its new)" do
17 | new_todo = TodoItem.new({title: "Adam is not getting this todo"})
18 | expect(new_todo).to be_changed
19 | end
20 |
21 | it "adds the todo to adam's todos and expects adam to change" do
22 | adam = User.find_by_first_name("Adam")
23 | adam.todo_items << TodoItem.find_by_title("Adam is not getting this todo")
24 | expect(adam).to be_changed
25 | end
26 |
27 | it "will show that the new todo is still changed" do
28 | expect(TodoItem.find_by_title("Adam is not getting this todo")).to be_changed
29 | end
30 |
31 | it "can be reverted and the todo will not be changed" do
32 | todo = TodoItem.find_by_title("Adam is not getting this todo")
33 | todo.revert
34 | expect(todo).not_to be_changed
35 | end
36 |
37 | it "will not have changed adam" do
38 | adam = User.find_by_first_name("Adam")
39 | expect(User.find_by_first_name("Adam")).not_to be_changed
40 | end
41 |
42 | it "is time to test going the other way, lets give adam a todo again" do
43 | new_todo = TodoItem.new({title: "Adam is still not getting this todo"})
44 | adam = User.find_by_first_name("Adam")
45 | adam.todo_items << new_todo
46 | expect(adam).to be_changed
47 | end
48 |
49 | it "an be revert" do
50 | adam = User.find_by_first_name("Adam")
51 | adam.revert
52 | expect(adam).not_to be_changed
53 | end
54 |
55 | it "finds the todo is still changed" do
56 | expect(TodoItem.find_by_title("Adam is still not getting this todo")).to be_changed
57 | end
58 |
59 | async "can change an attribute, revert, and make sure nothing else changes" do
60 | ReactiveRecord.load do
61 | User.find_by_email("mitch@catprint.com").last_name
62 | User.find_by_email("mitch@catprint.com").todo_items.all.count
63 | end.then do |original_todo_count|
64 | puts "current todo count for mitch: #{original_todo_count}"
65 | mitch = User.find_by_email("mitch@catprint.com")
66 | original_last_name = mitch.last_name
67 | mitch.last_name = "xxxx"
68 | mitch.save do
69 | mitch.revert
70 | new_items_count = mitch.todo_items.all.count
71 | mitch.last_name = original_last_name
72 | mitch.save.then do
73 | async { expect(new_items_count).to eq(original_todo_count) }
74 | end
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/scope_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "can scope models" do
4 |
5 | it "scopes todos by string" do
6 | React::IsomorphicHelpers.load_context
7 | ReactiveRecord.load do
8 | User.find_by_email("mitch@catprint.com").todo_items.find_string("mitch").first.title
9 | end.then do |title|
10 | expect(title).to be("a todo for mitch")
11 | end
12 | end
13 |
14 | it "can apply multiple simple scopes" do
15 | React::IsomorphicHelpers.load_context
16 | ReactiveRecord.load do
17 | User.find_by_email("mitch@catprint.com").todo_items.active.important.first.title
18 | end.then do |title|
19 | expect(title).to be("another todo for mitch")
20 | end
21 | end
22 |
23 | it "can apply multiple scopes" do
24 | ReactiveRecord.load do
25 | User.find_by_email("mitch@catprint.com").todo_items.find_string("mitch").find_string("another").count
26 | end.then do |count|
27 | expect(count).to be(1)
28 | end
29 | end
30 |
31 | it "can apply scopes to model" do
32 | ReactiveRecord.load do
33 | TodoItem.find_string("mitch").first.title
34 | end.then do |title|
35 | expect(title).to be("a todo for mitch")
36 | end
37 | end
38 |
39 | it "works for an empty set" do
40 | ReactiveRecord.load do
41 | User.find_by_email("adamg@catprint.com").todo_items.find_string("mitch").find_string("another").collect do |item|
42 | item.title
43 | end
44 | end.then do |result|
45 | expect(result).to eq([])
46 | end
47 | end
48 |
49 |
50 | it "works for an empty set even if other items are retrieved" do
51 | React::IsomorphicHelpers.load_context
52 | ReactiveRecord.load do
53 | user = User.find(3)
54 | user.todo_items.find_string("mitch").find_string("another").collect do |item|
55 | item.title
56 | end
57 | end.then do |result|
58 | expect(result).to eq([])
59 | end
60 | end
61 |
62 | it "reports that a collection is loading" do
63 | React::IsomorphicHelpers.load_context
64 | expect(User.find_by_email("mitch@catprint.com").todo_items).to be_loading
65 | end
66 |
67 | it "reports that a collection has loaded" do
68 | ReactiveRecord.load do
69 | User.find_by_email("mitch@catprint.com").todo_items.first.title
70 | end.then do |title|
71 | expect(User.find_by_email("mitch@catprint.com").todo_items).to be_loaded
72 | end
73 | end
74 |
75 | it "can return the count of a collection without loading the collection" do
76 | React::IsomorphicHelpers.load_context
77 | ReactiveRecord.load do
78 | User.find_by_email("mitch@catprint.com").todo_items.count
79 | end.then do |count|
80 | expect(count).to be(2)
81 | expect(User.find_by_email("mitch@catprint.com").todo_items).to be_loading
82 | end
83 | end
84 |
85 | end
86 |
--------------------------------------------------------------------------------
/lib/reactive_record/permissions.rb:
--------------------------------------------------------------------------------
1 | module ReactiveRecord
2 | class AccessViolation < StandardError
3 | def message
4 | "ReactiveRecord::AccessViolation: #{super}"
5 | end
6 | end
7 | end
8 |
9 | class ActiveRecord::Base
10 |
11 | attr_accessor :acting_user
12 |
13 | def create_permitted?
14 | true
15 | end
16 |
17 | def update_permitted?
18 | true
19 | end
20 |
21 | def destroy_permitted?
22 | true
23 | end
24 |
25 | def view_permitted?(attribute)
26 | true
27 | end
28 |
29 | def only_changed?(*attributes)
30 | (self.attributes.keys + self.class.reactive_record_association_keys).each do |key|
31 | return false if self.send("#{key}_changed?") and !attributes.include? key
32 | end
33 | true
34 | end
35 |
36 | def none_changed?(*attributes)
37 | attributes.each do |key|
38 | return false if self.send("#{key}_changed?")
39 | end
40 | true
41 | end
42 |
43 | def any_changed?(*attributes)
44 | attributes.each do |key|
45 | return true if self.send("#{key}_changed?")
46 | end
47 | false
48 | end
49 |
50 | def all_changed?(*attributes)
51 | attributes.each do |key|
52 | return false unless self.send("#{key}_changed?")
53 | end
54 | true
55 | end
56 |
57 | class << self
58 |
59 | attr_reader :reactive_record_association_keys
60 |
61 | [:has_many, :belongs_to, :composed_of].each do |macro|
62 | define_method "#{macro}_with_reactive_record_add_changed_method".to_sym do |attr_name, *args, &block|
63 | define_method "#{attr_name}_changed?".to_sym do
64 | instance_variable_get "@reactive_record_#{attr_name}_changed".to_sym
65 | end
66 | (@reactive_record_association_keys ||= []) << attr_name
67 | send "#{macro}_without_reactive_record_add_changed_method".to_sym, attr_name, *args, &block
68 | end
69 | alias_method_chain macro, :reactive_record_add_changed_method
70 | end
71 |
72 | def belongs_to_with_reactive_record_add_is_method(attr_name, scope = nil, options = {})
73 | define_method "#{attr_name}_is?".to_sym do |model|
74 | send(options[:foreign_key] || "#{attr_name}_id") == model.id
75 | end
76 | belongs_to_without_reactive_record_add_is_method(attr_name, scope, options)
77 | end
78 |
79 | alias_method_chain :belongs_to, :reactive_record_add_is_method
80 |
81 | end
82 |
83 |
84 | def check_permission_with_acting_user(user, permission, *args)
85 | old = acting_user
86 | self.acting_user = user
87 | if self.send(permission, *args)
88 | self.acting_user = old
89 | self
90 | else
91 | raise ReactiveRecord::AccessViolation, "for #{permission}(#{args})"
92 | end
93 | end
94 |
95 | end
96 |
97 | class ActionController::Base
98 |
99 | def acting_user
100 | end
101 |
102 | end
103 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/update_scopes_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "updating scopes" do
4 |
5 | # this spec needs some massive cleanup... the rendering tests continue to run... that needs to be fixed
6 |
7 | # the tests depend on each other.
8 |
9 | # there are no test for nested scopes like User.todos.active for example which will certainly fail
10 |
11 | rendering("saving a new record will update .all and cause a rerender") do
12 | unless @starting_count
13 | TodoItem.all.last.title
14 | unless TodoItem.all.count == 1
15 | @starting_count = TodoItem.all.count
16 |
17 | after(0.1) do
18 | TodoItem.new(title: "play it again sam").save
19 | end
20 | end
21 | end
22 | (TodoItem.all.count - (@starting_count || 100)).to_s
23 | end.should_generate do
24 | html == "1"
25 | end
26 |
27 | rendering("adding a new matching record will add the record to a scope using abbreviated to_sync macro") do
28 | unless @starting_count
29 | unless TodoItem.important.first.description.loading?
30 | @starting_count = TodoItem.important.count
31 | after(0.1) do
32 | td = TodoItem.new(description: "another big mitch todo XXX")
33 | td.save do
34 | puts "after save, pushing #{TodoItem.important} << #{td}"
35 | #TodoItem.important << td
36 | end
37 | end
38 | end
39 | end
40 | (TodoItem.important.count - (@starting_count || 100)).to_s
41 | end.should_generate do
42 | html == "1"
43 | end
44 |
45 | rendering("adding a new matching record will add the record to a scope using abbreviated to_sync macro") do
46 | unless @starting_count
47 | unless TodoItem.important.first.description.loading?
48 | @starting_count = TodoItem.important.count
49 | after(0.1) do
50 | TodoItem.new(description: "another big mitch todo XXX").save
51 | end
52 | end
53 | end
54 | (TodoItem.important.count - (@starting_count || 100)).to_s
55 | end.should_generate do
56 | html == "1"
57 | end
58 |
59 | rendering("adding a new matching record will add the record to a scope using full to_sync macro") do
60 | unless @starting_count
61 | unless TodoItem.active.first.title.loading?
62 | @starting_count = TodoItem.active.count
63 | after(0.1) do
64 | TodoItem.new(title: "another big mitch todo XXX").save
65 | end
66 | end
67 | end
68 | (TodoItem.active.count - (@starting_count || 100)).to_s
69 | end.should_generate do
70 | html == "1"
71 | end
72 |
73 | rendering("destroying records will cause a re-render") do
74 | unless @starting_count
75 | TodoItem.all.last.title
76 | unless TodoItem.all.count == 1
77 | @starting_count = TodoItem.all.count
78 | after(0.1) do
79 | TodoItem.all.last.destroy do
80 | TodoItem.all.last.destroy do
81 | TodoItem.all.last.destroy do
82 | TodoItem.all.last.destroy
83 | end
84 | end
85 | end
86 | end
87 | end
88 | end
89 | (TodoItem.all.count - (@starting_count || 100)).to_s
90 | end.should_generate do
91 | html == "-3"
92 | end
93 |
94 | end
95 |
--------------------------------------------------------------------------------
/spec/test_app/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require(*Rails.groups)
6 | require "reactive-record"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Settings in config/environments/* take precedence over those specified here.
11 | # Application configuration should go into files in config/initializers
12 | # -- all .rb files in that directory are automatically loaded.
13 |
14 | # Custom directories with classes and modules you want to be autoloadable.
15 |
16 | # Only load the plugins named here, in the order given (default is alphabetical).
17 | # :all can be used as a placeholder for all plugins not explicitly named.
18 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
19 |
20 | # Activate observers that should always be running.
21 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
22 |
23 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
24 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
25 | # config.time_zone = 'Central Time (US & Canada)'
26 |
27 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
28 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
29 | # config.i18n.default_locale = :de
30 |
31 | # Configure the default encoding used in templates for Ruby 1.9.
32 | config.encoding = "utf-8"
33 |
34 | # Configure sensitive parameters which will be filtered from the log file.
35 | config.filter_parameters += [:password]
36 |
37 | # Enable escaping HTML in JSON.
38 | config.active_support.escape_html_entities_in_json = true
39 |
40 | # Use SQL instead of Active Record's schema dumper when creating the database.
41 | # This is necessary if your schema can't be completely dumped by the schema dumper,
42 | # like if you have constraints or database-specific column types
43 | # config.active_record.schema_format = :sql
44 |
45 | # Enforce whitelist mode for mass assignment.
46 | # This will create an empty whitelist of attributes available for mass-assignment for all models
47 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
48 | # parameters by using an attr_accessible or attr_protected declaration.
49 | #config.active_record.whitelist_attributes = true
50 |
51 | # Enable the asset pipeline
52 | config.assets.enabled = true
53 |
54 | config.autoload_paths += %W(#{config.root}/app/views/models)
55 |
56 | # Version of your assets, change this if you want to expire all your assets
57 | config.assets.version = '1.0'
58 |
59 | # These are the available opal-rspec options with their default value:
60 | config.opal.method_missing = true
61 | config.opal.optimized_operators = true
62 | config.opal.arity_check = false
63 | config.opal.const_missing = true
64 | config.opal.dynamic_require_severity = :ignore
65 |
66 | # Enable/disable /opal_specs route
67 | config.opal.enable_specs = true
68 |
69 | config.opal.spec_location = 'spec-opal'
70 | end
71 | end
72 |
73 |
--------------------------------------------------------------------------------
/lib/reactive_record/active_record/instance_methods.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 |
3 | module InstanceMethods
4 |
5 | attr_reader :backing_record
6 |
7 | def attributes
8 | @backing_record.attributes
9 | end
10 |
11 | def initialize(hash = {})
12 |
13 | if hash.is_a? ReactiveRecord::Base
14 | @backing_record = hash
15 | else
16 | # standard active_record new -> creates a new instance, primary key is ignored if present
17 | # we have to build the backing record first then initialize it so associations work correctly
18 | @backing_record = ReactiveRecord::Base.new(self.class, {}, self)
19 | @backing_record.instance_eval do
20 | self.class.load_data do
21 | hash.each do |attribute, value|
22 | unless attribute == primary_key
23 | reactive_set!(attribute, value)
24 | changed_attributes << attribute
25 | end
26 | end
27 | #changed_attributes << primary_key # insures that changed attributes has at least one element
28 | end
29 | end
30 | end
31 | end
32 |
33 | def primary_key
34 | self.class.primary_key
35 | end
36 |
37 | def id
38 | @backing_record.reactive_get!(primary_key)
39 | end
40 |
41 | def id=(value)
42 | @backing_record.id = value
43 | end
44 |
45 | def model_name
46 | # in reality should return ActiveModel::Name object, blah blah
47 | self.class.model_name
48 | end
49 |
50 | def revert
51 | @backing_record.revert
52 | end
53 |
54 | def changed?
55 | @backing_record.changed?
56 | end
57 |
58 | def dup
59 | self.class.new(self.attributes)
60 | end
61 |
62 | def ==(ar_instance)
63 | @backing_record == ar_instance.instance_eval { @backing_record }
64 | end
65 |
66 | def method_missing(name, *args, &block)
67 | if name =~ /\!$/
68 | name = name.gsub(/\!$/,"")
69 | force_update = true
70 | end
71 | if name =~ /_changed\?$/
72 | @backing_record.changed?(name.gsub(/_changed\?$/,""))
73 | elsif args.count == 1 && name =~ /=$/ && !block
74 | attribute_name = name.gsub(/=$/,"")
75 | @backing_record.reactive_set!(attribute_name, args[0])
76 | elsif args.count == 0 && !block
77 | @backing_record.reactive_get!(name, force_update)
78 | elsif !block
79 | @backing_record.reactive_get!([[name]+args], force_update)
80 | else
81 | super
82 | end
83 | end
84 |
85 | def load(*attributes, &block)
86 | first_time = true
87 | ReactiveRecord.load do
88 | results = attributes.collect { |attr| @backing_record.reactive_get!(attr, first_time) }
89 | results = yield *results if block
90 | first_time = false
91 | block.nil? && results.count == 1 ? results.first : results
92 | end
93 | end
94 |
95 | def save(opts = {}, &block)
96 | @backing_record.save(opts.has_key?(:validate) ? opts[:validate] : true, opts[:force], &block)
97 | end
98 |
99 | def saving?
100 | @backing_record.saving?
101 | end
102 |
103 | def destroy(&block)
104 | @backing_record.destroy &block
105 | end
106 |
107 | def destroyed?
108 | @backing_record.destroyed
109 | end
110 |
111 | def new?
112 | @backing_record.new?
113 | end
114 |
115 | def errors
116 | React::State.get_state(@backing_record, @backing_record)
117 | @backing_record.errors
118 | end
119 |
120 | end
121 |
122 | end
123 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/base_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | #Opal::RSpec::Runner.autorun
3 |
4 | class BaseClass < ActiveRecord::Base
5 | end
6 |
7 | class SubClass < BaseClass
8 | end
9 |
10 | class Funky < ActiveRecord::Base
11 | self.primary_key = :funky_id
12 | self.inheritance_column = :funky_type
13 | end
14 |
15 | class BelongsTo < ActiveRecord::Base
16 | belongs_to :has_many
17 | belongs_to :has_one
18 | belongs_to :best_friend, class_name: "HasMany", foreign_key: :bf_id
19 | end
20 |
21 | class HasMany < ActiveRecord::Base
22 | has_many :belongs_to
23 | has_many :best_friends, class_name: "BelongsTo", foreign_key: :bf_id
24 | end
25 |
26 | class HasOne < ActiveRecord::Base
27 | has_one :belongs_to
28 | end
29 |
30 | class Scoped < ActiveRecord::Base
31 | scope :only_those_guys
32 | end
33 |
34 | describe "ActiveRecord" do
35 |
36 | before(:all) { React::IsomorphicHelpers.load_context }
37 |
38 | after(:each) { React::API.clear_component_class_cache }
39 |
40 | # uncomment if you are having trouble with tests failing. One non-async test must pass for things to work
41 |
42 | # describe "a passing dummy test" do
43 | # it "passes" do
44 | # expect(true).to be(true)
45 | # end
46 | # end
47 |
48 | describe "reactive_record active_record base methods" do
49 |
50 | it "will find the base class" do
51 | expect(SubClass.base_class).to eq(BaseClass)
52 | end
53 |
54 | it "knows the primary key" do
55 | expect(BaseClass.primary_key).to eq(:id)
56 | end
57 |
58 | it "can override the primary key" do
59 | expect(Funky.primary_key).to eq(:funky_id)
60 | end
61 |
62 | it "knows the inheritance column" do
63 | expect(BaseClass.inheritance_column).to eq(:type)
64 | end
65 |
66 | it "can override the inheritance column" do
67 | expect(Funky.inheritance_column).to eq(:funky_type)
68 | end
69 |
70 | it "knows the model name" do
71 | expect(BaseClass.model_name).to eq("BaseClass")
72 | end
73 |
74 | it "can find a record by id" do
75 | expect(BaseClass.find(12).id).to eq(12)
76 | end
77 |
78 | it "has a find_by_xxx method" do
79 | expect(BaseClass.find_by_xxx("beer").xxx).to eq("beer")
80 | end
81 |
82 | it "will correctly infer the model type from the inheritance column" do
83 | expect(BaseClass.find_by_type("SubClass").class).to eq(SubClass)
84 | expect(BaseClass.find_by_type(nil).class).to eq(BaseClass)
85 | end
86 |
87 | it "can have a has_many association" do
88 | expect(HasMany.reflect_on_association(:belongs_to).klass.reflect_on_association(:has_many).klass).to eq(HasMany)
89 | end
90 |
91 | it "can have a has_one association" do
92 | expect(HasOne.reflect_on_association(:belongs_to).klass.reflect_on_association(:has_one).klass).to eq(HasOne)
93 | end
94 |
95 | it "can override the class and foreign_key values when creating an association" do
96 | reflection = HasMany.reflect_on_association(:best_friends)
97 | expect(reflection.klass).to eq(BelongsTo)
98 | expect(reflection.association_foreign_key).to eq(:bf_id)
99 | end
100 |
101 | it "can have a scoping method" do
102 | expect(Scoped.only_those_guys.respond_to? :all).to be_truthy
103 | end
104 |
105 | it "can type check parameters" do
106 | expect(SubClass._react_param_conversion({attr1: 1, attr2: 2, type: "SubClass", id: 123}.to_n, :validate_only)).to be(true)
107 | end
108 |
109 | it "can type check parameters with native wrappers" do
110 | expect(SubClass._react_param_conversion(Native({attr1: 1, attr2: 2, type: "SubClass", id: 123}.to_n), :validate_only)).to be(true)
111 | end
112 |
113 | it "will fail type checking if type does not match" do
114 | expect(SubClass._react_param_conversion({attr1: 1, attr2: 2, type: nil, id: 123}.to_n, :validate_only)).to be_falsy
115 | end
116 |
117 | it "will convert a hash to an instance" do
118 | ar = SubClass._react_param_conversion({attr1: 1, attr2: 2, type: "SubClass", id: 123}.to_n)
119 | expect(ar.attr1).to eq(1)
120 | expect(ar.attr2).to eq(2)
121 | expect(ar.id).to eq(123)
122 | end
123 |
124 | end
125 |
126 | end
127 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/save_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "simple record update and save" do
4 |
5 | it "can find an existing model" do
6 | React::IsomorphicHelpers.load_context
7 | ReactiveRecord.load do
8 | User.find_by_email("mitch@catprint.com").first_name
9 | end.then do |first_name|
10 | expect(first_name).to be("Mitch")
11 | end
12 | end
13 |
14 | it "doesn't find the model changed" do
15 | expect(User.find_by_email("mitch@catprint.com")).not_to be_changed
16 | end
17 |
18 | it "the model is not new" do
19 | expect(User.find_by_email("mitch@catprint.com")).not_to be_new
20 | end
21 |
22 | it "the model is not saving" do
23 | expect(User.find_by_email("mitch@catprint.com")).not_to be_saving
24 | end
25 |
26 | it "an attribute can be changed" do
27 | mitch = User.find_by_email("mitch@catprint.com")
28 | mitch.first_name = "Mitchell"
29 | expect(mitch.first_name).to eq("Mitchell")
30 | end
31 |
32 | it "and the attribute will be marked as changed"do
33 | expect(User.find_by_email("mitch@catprint.com")).to be_changed
34 | end
35 |
36 | it "saving? is true while the model is being saved" do
37 | mitch = User.find_by_email("mitch@catprint.com")
38 | mitch.save.then {}.tap { expect(mitch).to be_saving }
39 | end
40 |
41 | it "after saving changed? will be false" do
42 | expect(User.find_by_email("mitch@catprint.com")).not_to be_changed
43 | end
44 |
45 | it "after saving saving? will be false" do
46 | expect(User.find_by_email("mitch@catprint.com")).not_to be_saving
47 | end
48 |
49 | it "the data has been persisted to the database" do
50 | React::IsomorphicHelpers.load_context
51 | ReactiveRecord.load do
52 | User.find_by_email("mitch@catprint.com").first_name
53 | end.then do |first_name|
54 | expect(first_name).to eq("Mitchell")
55 | end
56 | end
57 |
58 | it "after saving within the block saving? will be false" do
59 | mitchell = User.find_by_email("mitch@catprint.com")
60 | mitchell.first_name = "Mitch"
61 | mitchell.save.then do
62 | expect(mitchell).not_to be_saving
63 | end
64 | end
65 |
66 | async "the save block receives the correct block parameters" do
67 | mitch = User.find_by_email("mitch@catprint.com")
68 | mitch.first_name = "Mitchell"
69 | mitch.save do | success, message, models |
70 | async do
71 | expect(success).to be_truthy
72 | expect(message).to be_nil
73 | expect(models).to eq([mitch])
74 | expect(mitch.errors).to be_empty
75 | end
76 | end
77 | end
78 |
79 | it "the save promise receives the response hash" do
80 | mitch = User.find_by_email("mitch@catprint.com")
81 | mitch.first_name = "Mitch"
82 | mitch.save.then do | response |
83 | expect(response[:success]).to be_truthy
84 | expect(response[:message]).to be_nil
85 | expect(response[:models]).to eq([mitch])
86 | end
87 | end
88 |
89 | async "the save will fail if validation fails" do
90 | mitch = User.find_by_email("mitch@catprint.com")
91 | mitch.email = "mitch at catprint dot com"
92 | mitch.save do |success, message, models|
93 | async do
94 | expect(success).to be_falsy
95 | expect(message).to be_present
96 | expect(models).to eq([mitch])
97 | end
98 | end
99 | end
100 |
101 | it "validation errors are put in the errors object" do
102 | mitch = User.find_by_email("mitch@catprint.com")
103 | mitch.email = "mitch at catprint dot com"
104 | mitch.save.then do |success, message, models|
105 | expect(mitch.errors[:email]).to eq(["is invalid"])
106 | end
107 | end
108 |
109 | it "within the save block saving? is false if validation fails" do
110 | mitch = User.find_by_email("mitch@catprint.com")
111 | mitch.email = "mitch at catprint dot com"
112 | mitch.save.then do
113 | expect(mitch).not_to be_saving
114 | end
115 | end
116 |
117 | it "if validation fails changed? is still true" do
118 | mitch = User.find_by_email("mitch@catprint.com")
119 | mitch.email = "mitch at catprint dot com"
120 | mitch.save.then do
121 | expect(mitch).to be_changed
122 | end
123 | end
124 |
125 | end
126 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/edge_cases_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "pending edge cases" do
4 |
5 | before(:each) do
6 | React::IsomorphicHelpers.load_context
7 | end
8 |
9 | it "base and subclass both belong to same parent record"
10 | it "will set changed on the parent record when updating a child aggregate"
11 |
12 | it "knows a targets owner before loading" do
13 | React::IsomorphicHelpers.load_context
14 | expect(User.find_by_email("mitch@catprint.com").todo_items.first.user.email).to eq("mitch@catprint.com")
15 | end
16 |
17 | it "can return a nil association" do
18 | ReactiveRecord.load do
19 | TodoItem.all.collect do |todo|
20 | todo.comment and todo.comment.comment
21 | end.compact
22 | end.then do |collection|
23 | expect(collection).to be_empty
24 | end
25 | end
26 |
27 | it "trims the association tree" do
28 | ReactiveRecord.load do
29 | TodoItem.all.collect do |todo|
30 | todo.user && todo.user.first_name
31 | end.compact
32 | end.then do |first_names|
33 | expect(first_names.count).to eq(5)
34 | end
35 | end
36 |
37 | async "reruns loads in order" do
38 | User.find_by_email("mitch@catprint.com").last_name
39 | after(0.01) do
40 | ReactiveRecord.load do
41 | TodoItem.find_by_title("do it again Todd").description
42 | end.then do |description|
43 | async { expect(description).to eq("Todd please do that great thing you did again") }
44 | end
45 | end
46 | end
47 |
48 | async "will load the same record via two different methods" do
49 | ReactiveRecord.load do
50 | # first load a record one way
51 | # on load retry we want to just insure the contents are loaded, but we are still pointing the same instance
52 | @r1 ||= User.find_by_email("mitch@catprint.com")
53 | @r1.address.zip # just so we grab something that is not the id
54 | @r1
55 | end.then do |r1|
56 | ReactiveRecord.load do
57 | # now repeat but get teh record a different way, this will return a different instance
58 | @r2 ||= User.find_by_first_name("Mitch")
59 | @r2.last_name # lets get the last name, when loaded the two record ids will match and will be merged
60 | @r2
61 | end.then do |r2|
62 | async do
63 | expect(r1.last_name).to eq(r2.last_name)
64 | expect(r1).to eq(r2)
65 | expect(r1).not_to be(r2)
66 | end
67 | end
68 | end
69 | end
70 |
71 | async "will load the same record via two different methods via a collection" do
72 | ReactiveRecord.load do
73 | # first load a record one way
74 | # on load retry we want to just insure the contents are loaded, but we are still pointing the same instance
75 | @r1 ||= User.find_by_email("mitch@catprint.com").todo_items.first
76 | @r1.title # just so we grab something that is not the id
77 | @r1
78 | end.then do |r1|
79 | ReactiveRecord.load do
80 | # now repeat but get teh record a different way, this will return a different instance
81 | @r2 ||= TodoItem.find_by_title("#{r1.title}") # to make sure there is no magic lets make the title into a new string
82 | @r2.description # lets get the description, when loaded the two record ids will match and will be merged
83 | @r2
84 | end.then do |r2|
85 | async do
86 | expect(r1.description).to eq(r2.description)
87 | expect(r1).to eq(r2)
88 | expect(r1).not_to be(r2)
89 | end
90 | end
91 | end
92 | end
93 |
94 | async "will load a record by indexing a collection" do
95 | ReactiveRecord.load do
96 | User.find_by_email("mitch@catprint.com").todo_items.collect { |todo| todo.description }
97 | end.then do |descriptions|
98 | React::IsomorphicHelpers.load_context
99 | ReactiveRecord.load do
100 | User.find_by_email("mitch@catprint.com").todo_items[1].description
101 | end.then do |description|
102 | async { expect(description).to eq(descriptions[1]) }
103 | end
104 | end
105 | end
106 |
107 | it "will load nested collections correctly" do
108 | ReactiveRecord.load do
109 | User.find_by_email("test1@catprint.com").todo_items.collect do |todo|
110 | todo.comments.collect do |comment|
111 | comment.comment
112 | end
113 | end
114 | end.then do | comments |
115 | expect(comments).to eq([["test 1 todo 1 comment 1", "test 1 todo 1 comment 2"],["test 1 todo 2 comment 1", "test 1 todo 2 comment 2"]])
116 | end
117 | end
118 |
119 | async "will not fetch model.all when saving a new record to the model" do
120 | (new_record = User.new(email: "test22@catprint.com")).save do
121 | new_record.destroy.then do
122 | async { expect(ReactiveRecord::Base.class_scopes(User)[:all]).to be_nil }
123 | end
124 | end
125 | end
126 |
127 | end
128 |
--------------------------------------------------------------------------------
/lib/reactive_record/interval.rb:
--------------------------------------------------------------------------------
1 | module Browser
2 |
3 | # Allows you to create an interval that executes the function every given
4 | # seconds.
5 | #
6 | # @see https://developer.mozilla.org/en-US/docs/Web/API/Window.setInterval
7 | class Interval
8 | # @!attribute [r] every
9 | # @return [Float] the seconds every which the block is called
10 | attr_reader :every
11 |
12 | # Create and start an interval.
13 | #
14 | # @param window [Window] the window to start the interval on
15 | # @param time [Float] seconds every which to call the block
16 | def initialize(window, time, &block)
17 | @window = Native.convert(window)
18 | @every = time
19 | @block = block
20 |
21 | @aborted = false
22 | end
23 |
24 | # Check if the interval has been stopped.
25 | def stopped?
26 | @id.nil?
27 | end
28 |
29 | # Check if the interval has been aborted.
30 | def aborted?
31 | @aborted
32 | end
33 |
34 | # Abort the interval, it won't be possible to start it again.
35 | def abort
36 | `#@window.clearInterval(#@id)`
37 |
38 | @aborted = true
39 | @id = nil
40 | end
41 |
42 | # Stop the interval, it will be possible to start it again.
43 | def stop
44 | return if stopped?
45 |
46 | `#@window.clearInterval(#@id)`
47 |
48 | @stopped = true
49 | @id = nil
50 | end
51 |
52 | # Start the interval if it has been stopped.
53 | def start
54 | raise "the interval has been aborted" if aborted?
55 | return unless stopped?
56 |
57 | @id = `#@window.setInterval(#@block, #@every * 1000)`
58 | end
59 |
60 | # Call the [Interval] block.
61 | def call
62 | @block.call
63 | end
64 | end
65 |
66 | class Window
67 | # Execute the block every given seconds.
68 | #
69 | # @param time [Float] the seconds between every call
70 | #
71 | # @return [Interval] the object representing the interval
72 | def every(time, &block)
73 | Interval.new(@native, time, &block).tap(&:start)
74 | end
75 |
76 | # Execute the block every given seconds, you have to call [#start] on it
77 | # yourself.
78 | #
79 | # @param time [Float] the seconds between every call
80 | #
81 | # @return [Interval] the object representing the interval
82 | def every!(time, &block)
83 | Interval.new(@native, time, &block)
84 | end
85 | end
86 |
87 | end
88 |
89 | module Kernel
90 | # (see Browser::Window#every)
91 | def every(time, &block)
92 | $window.every(time, &block)
93 | end
94 |
95 | # (see Browser::Window#every!)
96 | def every!(time, &block)
97 | $window.every!(time, &block)
98 | end
99 | end
100 |
101 | class Proc
102 | # (see Browser::Window#every)
103 | def every(time)
104 | $window.every(time, &self)
105 | end
106 |
107 | # (see Browser::Window#every!)
108 | def every!(time)
109 | $window.every!(time, &self)
110 | end
111 | end
112 |
113 | module Browser
114 |
115 | # Allows you to delay the call to a function which gets called after the
116 | # given time.
117 | #
118 | # @see https://developer.mozilla.org/en-US/docs/Web/API/Window.setTimeout
119 | class Delay
120 | # @!attribute [r] after
121 | # @return [Float] the seconds after which the block is called
122 | attr_reader :after
123 |
124 | # Create and start a timeout.
125 | #
126 | # @param window [Window] the window to start the timeout on
127 | # @param time [Float] seconds after which the block is called
128 | def initialize(window, time, &block)
129 | @window = Native.convert(window)
130 | @after = time
131 | @block = block
132 | end
133 |
134 | # Abort the timeout.
135 | def abort
136 | `#@window.clearTimeout(#@id)`
137 | end
138 |
139 | # Start the delay.
140 | def start
141 | @id = `#@window.setTimeout(#{@block.to_n}, #@after * 1000)`
142 | end
143 | end
144 |
145 | class Window
146 | # Execute a block after the given seconds.
147 | #
148 | # @param time [Float] the seconds after it gets called
149 | #
150 | # @return [Delay] the object representing the timeout
151 | def after(time, &block)
152 | Delay.new(@native, time, &block).tap(&:start)
153 | end
154 |
155 | # Execute a block after the given seconds, you have to call [#start] on it
156 | # yourself.
157 | #
158 | # @param time [Float] the seconds after it gets called
159 | #
160 | # @return [Delay] the object representing the timeout
161 | def after!(time, &block)
162 | Delay.new(@native, time, &block)
163 | end
164 | end
165 |
166 | end
167 |
168 | module Kernel
169 | # (see Browser::Window#after)
170 | def after(time, &block)
171 | `setTimeout(#{block.to_n}, time * 1000)`
172 | end
173 |
174 | # (see Browser::Window#after!)
175 | def after!(time, &block)
176 | `setTimeout(#{block.to_n}, time * 1000)`
177 | end
178 | end
179 |
180 | class Proc
181 | # (see Browser::Window#after)
182 | def after(time)
183 | $window.after(time, &self)
184 | end
185 |
186 | # (see Browser::Window#after!)
187 | def after!(time)
188 | $window.after!(time, &self)
189 | end
190 | end
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/update_associations_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "updating associations" do
4 |
5 | before(:all) do
6 | React::IsomorphicHelpers.load_context
7 | User.new({first_name: "Jon", last_name: "Weaver"})
8 | end
9 |
10 | it "a new model will have empty has_many assocation" do
11 | jon = User.find_by_first_name("Jon")
12 | expect(jon.todo_items).to be_empty
13 | end
14 |
15 | it "an item can be added to a has_many association" do
16 | jon = User.find_by_first_name("Jon")
17 | result = (jon.todo_items << (item = TodoItem.new({title: "Jon's first todo!"})))
18 | expect(result).to be(jon.todo_items)
19 | expect(jon.todo_items.count).to be(1)
20 | end
21 |
22 | async "it will persist the new has_many association" do
23 | User.find_by_first_name("Jon").save do
24 | React::IsomorphicHelpers.load_context
25 | ReactiveRecord.load do
26 | User.find_by_first_name("Jon").todo_items.count
27 | end.then do | count |
28 | async { expect(count).to be(1) }
29 | end
30 | end
31 | end
32 |
33 | it "and will reconstruct the association and values on reloading" do
34 | ReactiveRecord.load do
35 | User.find_by_first_name("Jon").todo_items.collect { | todo | todo.title }
36 | end.then do | titles |
37 | expect(titles).to eq(["Jon's first todo!"])
38 | end
39 | end
40 |
41 | it "the inverse belongs_to association will be set" do
42 | todo = TodoItem.find_by_title("Jon's first todo!")
43 | expect(todo.user.first_name).to eq("Jon")
44 | end
45 |
46 | it "a model can be moved to a new owner, and will be removed from the old owner" do
47 | TodoItem.find_by_title("Jon's first todo!").user = User.new({first_name: "Jan", last_name: "VanDuyn"})
48 | expect(User.find_by_first_name("Jon").todo_items).to be_empty
49 | end
50 |
51 | it "and will belong to the new owner" do
52 | expect(User.find_by_first_name("Jan").todo_items.all == [TodoItem.find_by_title("Jon's first todo!")]).to be_truthy
53 | end
54 |
55 | async "and can be saved and it will remember its new owner" do
56 | TodoItem.find_by_title("Jon's first todo!").save do
57 | React::IsomorphicHelpers.load_context
58 | ReactiveRecord.load do
59 | TodoItem.find_by_title("Jon's first todo!").user.first_name
60 | end.then do | first_name |
61 | async { expect(first_name).to be("Jan") }
62 | end
63 | end
64 | end
65 |
66 | it "and after saving will have been removed from original owners association" do
67 | ReactiveRecord.load do
68 | User.find_by_first_name("Jon").todo_items.all
69 | end.then do | todos |
70 | expect(todos).to be_empty
71 | end
72 | end
73 |
74 | it "a belongs to association can be set to nil and the model saved" do
75 | todo = TodoItem.find_by_title("Jon's first todo!")
76 | todo.user = nil
77 | todo.save.then do | response |
78 | expect(response[:success]).to be_truthy
79 | end
80 | end
81 |
82 | it "and will not belong to the previous owner anymore" do
83 | React::IsomorphicHelpers.load_context
84 | ReactiveRecord.load do
85 | TodoItem.find_by_title("Jon's first todo!").user # load the todo in prep for the next test
86 | User.find_by_first_name("Jan").todo_items.all.count
87 | end.then do |count|
88 | expect(count).to be(0)
89 | end
90 | end
91 |
92 | it "but can be reassigned to the previous owner" do
93 | todo = TodoItem.find_by_title("Jon's first todo!")
94 | todo.user = User.find_by_first_name("Jan")
95 | todo.save.then do | response |
96 | expect(response[:success]).to be_truthy
97 | end
98 | end
99 |
100 | it "and a model in a belongs_to relationship can be deleted" do
101 | User.find_by_first_name("Jan").todo_items.first.destroy.then do
102 | expect(User.find_by_first_name("Jan").todo_items).to be_empty
103 | end
104 | end
105 |
106 | it "and it won't exist" do
107 | React::IsomorphicHelpers.load_context
108 | ReactiveRecord.load do
109 | TodoItem.find_by_title("Jon's first todo!").id
110 | end.then do | id |
111 | expect(id).to be_nil
112 | end
113 | end
114 |
115 | it "an item in a belongs_to relationship can be created without belonging to anybody" do
116 | nobodys_business = TodoItem.new({title: "round to it"})
117 | nobodys_business.save.then do |saved|
118 | expect(saved).to be_truthy
119 | end
120 | end
121 |
122 | it "and can be reloaded" do
123 | React::IsomorphicHelpers.load_context
124 | ReactiveRecord.load do
125 | TodoItem.find_by_title("round to it").id
126 | end.then do |id|
127 | expect(id).not_to be_nil
128 | end
129 | end
130 |
131 | it "and can be deleted" do
132 | TodoItem.find_by_title("round to it").destroy.then do
133 | expect(TodoItem.find_by_title("round to it")).to be_destroyed
134 | end
135 | end
136 |
137 | after(:all) do
138 | Promise.when(User.find_by_first_name("Jan").destroy, User.find_by_first_name("Jon").destroy)
139 | end
140 |
141 |
142 | end
143 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | reactive-record (0.8.2)
5 | opal-browser
6 | opal-rails
7 | rails (>= 3.2.13)
8 | react-rails
9 | reactrb
10 |
11 | GEM
12 | specs:
13 | actionmailer (4.2.6)
14 | actionpack (= 4.2.6)
15 | actionview (= 4.2.6)
16 | activejob (= 4.2.6)
17 | mail (~> 2.5, >= 2.5.4)
18 | rails-dom-testing (~> 1.0, >= 1.0.5)
19 | actionpack (4.2.6)
20 | actionview (= 4.2.6)
21 | activesupport (= 4.2.6)
22 | rack (~> 1.6)
23 | rack-test (~> 0.6.2)
24 | rails-dom-testing (~> 1.0, >= 1.0.5)
25 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
26 | actionview (4.2.6)
27 | activesupport (= 4.2.6)
28 | builder (~> 3.1)
29 | erubis (~> 2.7.0)
30 | rails-dom-testing (~> 1.0, >= 1.0.5)
31 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
32 | activejob (4.2.6)
33 | activesupport (= 4.2.6)
34 | globalid (>= 0.3.0)
35 | activemodel (4.2.6)
36 | activesupport (= 4.2.6)
37 | builder (~> 3.1)
38 | activerecord (4.2.6)
39 | activemodel (= 4.2.6)
40 | activesupport (= 4.2.6)
41 | arel (~> 6.0)
42 | activesupport (4.2.6)
43 | i18n (~> 0.7)
44 | json (~> 1.7, >= 1.7.7)
45 | minitest (~> 5.1)
46 | thread_safe (~> 0.3, >= 0.3.4)
47 | tzinfo (~> 1.1)
48 | arel (6.0.3)
49 | babel-source (5.8.35)
50 | babel-transpiler (0.7.0)
51 | babel-source (>= 4.0, < 6)
52 | execjs (~> 2.0)
53 | builder (3.2.2)
54 | coderay (1.1.1)
55 | coffee-script-source (1.10.0)
56 | concurrent-ruby (1.0.2)
57 | connection_pool (2.2.0)
58 | diff-lcs (1.2.5)
59 | erubis (2.7.0)
60 | execjs (2.7.0)
61 | globalid (0.3.7)
62 | activesupport (>= 4.1.0)
63 | hike (1.2.3)
64 | i18n (0.7.0)
65 | jquery-rails (4.1.1)
66 | rails-dom-testing (>= 1, < 3)
67 | railties (>= 4.2.0)
68 | thor (>= 0.14, < 2.0)
69 | json (1.8.3)
70 | libv8 (3.16.14.15)
71 | loofah (2.0.3)
72 | nokogiri (>= 1.5.9)
73 | mail (2.6.4)
74 | mime-types (>= 1.16, < 4)
75 | method_source (0.8.2)
76 | mime-types (3.1)
77 | mime-types-data (~> 3.2015)
78 | mime-types-data (3.2016.0521)
79 | mini_portile2 (2.1.0)
80 | minitest (5.9.0)
81 | nokogiri (1.6.8)
82 | mini_portile2 (~> 2.1.0)
83 | pkg-config (~> 1.1.7)
84 | opal (0.9.4)
85 | hike (~> 1.2)
86 | sourcemap (~> 0.1.0)
87 | sprockets (~> 3.1)
88 | tilt (>= 1.4)
89 | opal-activesupport (0.3.0)
90 | opal (>= 0.5.0, < 1.0.0)
91 | opal-browser (0.2.0)
92 | opal
93 | paggio
94 | opal-jquery (0.4.2)
95 | opal (>= 0.7.0, < 0.11.0)
96 | opal-rails (0.9.0)
97 | jquery-rails
98 | opal (>= 0.8.0, < 0.11)
99 | opal-activesupport (>= 0.0.5)
100 | opal-jquery (~> 0.4.0)
101 | rails (>= 4.0, < 6.0)
102 | sprockets-rails (< 3.0)
103 | paggio (0.2.6)
104 | pkg-config (1.1.7)
105 | pry (0.10.3)
106 | coderay (~> 1.1.0)
107 | method_source (~> 0.8.1)
108 | slop (~> 3.4)
109 | rack (1.6.4)
110 | rack-test (0.6.3)
111 | rack (>= 1.0)
112 | rails (4.2.6)
113 | actionmailer (= 4.2.6)
114 | actionpack (= 4.2.6)
115 | actionview (= 4.2.6)
116 | activejob (= 4.2.6)
117 | activemodel (= 4.2.6)
118 | activerecord (= 4.2.6)
119 | activesupport (= 4.2.6)
120 | bundler (>= 1.3.0, < 2.0)
121 | railties (= 4.2.6)
122 | sprockets-rails
123 | rails-deprecated_sanitizer (1.0.3)
124 | activesupport (>= 4.2.0.alpha)
125 | rails-dom-testing (1.0.7)
126 | activesupport (>= 4.2.0.beta, < 5.0)
127 | nokogiri (~> 1.6.0)
128 | rails-deprecated_sanitizer (>= 1.0.1)
129 | rails-html-sanitizer (1.0.3)
130 | loofah (~> 2.0)
131 | railties (4.2.6)
132 | actionpack (= 4.2.6)
133 | activesupport (= 4.2.6)
134 | rake (>= 0.8.7)
135 | thor (>= 0.18.1, < 2.0)
136 | rake (11.2.2)
137 | react-rails (1.8.2)
138 | babel-transpiler (>= 0.7.0)
139 | coffee-script-source (~> 1.8)
140 | connection_pool
141 | execjs
142 | railties (>= 3.2)
143 | tilt
144 | reactrb (0.8.8)
145 | opal (>= 0.8.0)
146 | opal-activesupport (>= 0.2.0)
147 | opal-browser (= 0.2.0)
148 | ref (2.0.0)
149 | rspec-core (3.4.4)
150 | rspec-support (~> 3.4.0)
151 | rspec-expectations (3.4.0)
152 | diff-lcs (>= 1.2.0, < 2.0)
153 | rspec-support (~> 3.4.0)
154 | rspec-mocks (3.4.1)
155 | diff-lcs (>= 1.2.0, < 2.0)
156 | rspec-support (~> 3.4.0)
157 | rspec-rails (3.4.2)
158 | actionpack (>= 3.0, < 4.3)
159 | activesupport (>= 3.0, < 4.3)
160 | railties (>= 3.0, < 4.3)
161 | rspec-core (~> 3.4.0)
162 | rspec-expectations (~> 3.4.0)
163 | rspec-mocks (~> 3.4.0)
164 | rspec-support (~> 3.4.0)
165 | rspec-support (3.4.1)
166 | slop (3.6.0)
167 | sourcemap (0.1.1)
168 | sprockets (3.7.0)
169 | concurrent-ruby (~> 1.0)
170 | rack (> 1, < 3)
171 | sprockets-rails (2.3.3)
172 | actionpack (>= 3.0)
173 | activesupport (>= 3.0)
174 | sprockets (>= 2.8, < 4.0)
175 | sqlite3 (1.3.11)
176 | therubyracer (0.12.2)
177 | libv8 (~> 3.16.14.0)
178 | ref
179 | thor (0.19.1)
180 | thread_safe (0.3.5)
181 | tilt (2.0.5)
182 | tzinfo (1.2.2)
183 | thread_safe (~> 0.1)
184 |
185 | PLATFORMS
186 | ruby
187 |
188 | DEPENDENCIES
189 | pry
190 | reactive-record!
191 | rspec-rails
192 | sqlite3
193 | therubyracer
194 |
195 | BUNDLED WITH
196 | 1.12.5
197 |
--------------------------------------------------------------------------------
/spec/test_app/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] ||= 'test'
2 | require File.expand_path("../../config/environment", __FILE__)
3 | require 'rspec/rails'
4 |
5 | #require 'rspec'
6 | #require 'rspec/expectations'
7 | #require 'shoulda/matchers'
8 | #require 'database_cleaner'
9 | # require "bundler/setup"
10 | # Bundler.setup
11 | # require "rails"
12 | require 'reactive-record'
13 |
14 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all
15 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
16 | # The generated `.rspec` file contains `--require spec_helper` which will cause this
17 | # file to always be loaded, without a need to explicitly require it in any files.
18 | #
19 | # Given that it is always loaded, you are encouraged to keep this file as
20 | # light-weight as possible. Requiring heavyweight dependencies from this file
21 | # will add to the boot time of your test suite on EVERY test run, even for an
22 | # individual file that may not need all of that loaded. Instead, consider making
23 | # a separate helper file that requires the additional dependencies and performs
24 | # the additional setup, and require it from the spec files that actually need it.
25 | #
26 | # The `.rspec` file also contains a few flags that are not defaults but that
27 | # users commonly want.
28 | #
29 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
30 | RSpec.configure do |config|
31 | # rspec-expectations config goes here. You can use an alternate
32 | # assertion/expectation library such as wrong or the stdlib/minitest
33 | # assertions if you prefer.
34 | config.expect_with :rspec do |expectations|
35 | # Enable only the newer, non-monkey-patching expect syntax.
36 | # For more details, see:
37 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
38 | expectations.syntax = [:should, :expect]
39 | end
40 |
41 | # rspec-mocks config goes here. You can use an alternate test double
42 | # library (such as bogus or mocha) by changing the `mock_with` option here.
43 | config.mock_with :rspec do |mocks|
44 | # Enable only the newer, non-monkey-patching expect syntax.
45 | # For more details, see:
46 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
47 | mocks.syntax = :expect
48 |
49 | # Prevents you from mocking or stubbing a method that does not exist on
50 | # a real object. This is generally recommended.
51 | mocks.verify_partial_doubles = true
52 | end
53 |
54 | #config.use_transactional_fixtures = false
55 |
56 | # DatabaseCleaner.strategy = :truncation
57 |
58 | # config.before(:suite) do
59 | # begin
60 | # DatabaseCleaner.clean
61 | # DatabaseCleaner.start
62 | # FactoryGirl.lint
63 | # ensure
64 | # DatabaseCleaner.clean
65 | # end
66 | # end
67 |
68 | # config.after(:suite) do
69 | # DatabaseCleaner.clean
70 | # end
71 |
72 | # config.before(:suite) do
73 | # DatabaseCleaner.clean_with(:truncation)
74 | # end
75 | #
76 | # config.before(:each) do
77 | # DatabaseCleaner.strategy = :transaction
78 | # end
79 | #
80 | # config.before(:each, :js => true) do
81 | # DatabaseCleaner.strategy = :truncation
82 | # end
83 | #
84 | # config.before(:each) do
85 | # DatabaseCleaner.start
86 | # end
87 | #
88 | # config.after(:each) do
89 | # DatabaseCleaner.clean
90 | # end
91 |
92 | # The settings below are suggested to provide a good initial experience
93 | # with RSpec, but feel free to customize to your heart's content.
94 | # =begin
95 | # # These two settings work together to allow you to limit a spec run
96 | # # to individual examples or groups you care about by tagging them with
97 | # # `:focus` metadata. When nothing is tagged with `:focus`, all examples
98 | # # get run.
99 | # config.filter_run :focus
100 | # config.run_all_when_everything_filtered = true
101 | #
102 | # # Limits the available syntax to the non-monkey patched syntax that is recommended.
103 | # # For more details, see:
104 | # # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
105 | # # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
106 | # # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
107 | # config.disable_monkey_patching!
108 | #
109 | # # Many RSpec users commonly either run the entire suite or an individual
110 | # # file, and it's useful to allow more verbose output when running an
111 | # # individual spec file.
112 | # if config.files_to_run.one?
113 | # # Use the documentation formatter for detailed output,
114 | # # unless a formatter has already been configured
115 | # # (e.g. via a command-line flag).
116 | # config.default_formatter = 'doc'
117 | # end
118 | #
119 | # # Print the 10 slowest examples and example groups at the
120 | # # end of the spec run, to help surface which specs are running
121 | # # particularly slow.
122 | # config.profile_examples = 10
123 | #
124 | # # Run specs in random order to surface order dependencies. If you find an
125 | # # order dependency and want to debug it, you can fix the order by providing
126 | # # the seed, which is printed after each run.
127 | # # --seed 1234
128 | # config.order = :random
129 | #
130 | # # Seed global randomization in this process using the `--seed` CLI option.
131 | # # Setting this allows you to use `--seed` to deterministically reproduce
132 | # # test failures related to randomization by passing the same `--seed` value
133 | # # as the one that triggered the failure.
134 | # Kernel.srand config.seed
135 | # =end
136 |
137 | end
138 |
--------------------------------------------------------------------------------
/spec/test_app/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: git://github.com/opal/opal-rspec-rails.git
3 | revision: 0b8dc2745bee1aa59fe56323cc6d6493d2129cfc
4 | specs:
5 | opal-rspec-rails (0.1.0)
6 | opal (~> 0.9.2)
7 | opal-rails (~> 0.9.0.dev)
8 | opal-rspec (~> 0.5.0)
9 |
10 | PATH
11 | remote: ../..
12 | specs:
13 | reactive-record (0.8.3)
14 | opal-browser
15 | opal-rails
16 | rails (>= 3.2.13)
17 | react-rails
18 | reactrb
19 |
20 | GEM
21 | remote: http://rubygems.org/
22 | specs:
23 | actionmailer (4.2.7)
24 | actionpack (= 4.2.7)
25 | actionview (= 4.2.7)
26 | activejob (= 4.2.7)
27 | mail (~> 2.5, >= 2.5.4)
28 | rails-dom-testing (~> 1.0, >= 1.0.5)
29 | actionpack (4.2.7)
30 | actionview (= 4.2.7)
31 | activesupport (= 4.2.7)
32 | rack (~> 1.6)
33 | rack-test (~> 0.6.2)
34 | rails-dom-testing (~> 1.0, >= 1.0.5)
35 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
36 | actionview (4.2.7)
37 | activesupport (= 4.2.7)
38 | builder (~> 3.1)
39 | erubis (~> 2.7.0)
40 | rails-dom-testing (~> 1.0, >= 1.0.5)
41 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
42 | activejob (4.2.7)
43 | activesupport (= 4.2.7)
44 | globalid (>= 0.3.0)
45 | activemodel (4.2.7)
46 | activesupport (= 4.2.7)
47 | builder (~> 3.1)
48 | activerecord (4.2.7)
49 | activemodel (= 4.2.7)
50 | activesupport (= 4.2.7)
51 | arel (~> 6.0)
52 | activesupport (4.2.7)
53 | i18n (~> 0.7)
54 | json (~> 1.7, >= 1.7.7)
55 | minitest (~> 5.1)
56 | thread_safe (~> 0.3, >= 0.3.4)
57 | tzinfo (~> 1.1)
58 | arel (6.0.3)
59 | babel-source (5.8.35)
60 | babel-transpiler (0.7.0)
61 | babel-source (>= 4.0, < 6)
62 | execjs (~> 2.0)
63 | builder (3.2.2)
64 | byebug (9.0.5)
65 | coderay (1.1.1)
66 | coffee-script-source (1.10.0)
67 | concurrent-ruby (1.0.2)
68 | connection_pool (2.2.0)
69 | erubis (2.7.0)
70 | execjs (2.7.0)
71 | globalid (0.3.7)
72 | activesupport (>= 4.1.0)
73 | hike (1.2.3)
74 | i18n (0.7.0)
75 | interception (0.5)
76 | jquery-cookie-rails (1.3.1.1)
77 | railties (>= 3.2.0, < 5.0)
78 | jquery-rails (4.1.1)
79 | rails-dom-testing (>= 1, < 3)
80 | railties (>= 4.2.0)
81 | thor (>= 0.14, < 2.0)
82 | json (1.8.3)
83 | libv8 (3.16.14.15)
84 | loofah (2.0.3)
85 | nokogiri (>= 1.5.9)
86 | mail (2.6.4)
87 | mime-types (>= 1.16, < 4)
88 | method_source (0.8.2)
89 | mime-types (3.1)
90 | mime-types-data (~> 3.2015)
91 | mime-types-data (3.2016.0521)
92 | mini_portile2 (2.1.0)
93 | minitest (5.9.0)
94 | nokogiri (1.6.8)
95 | mini_portile2 (~> 2.1.0)
96 | pkg-config (~> 1.1.7)
97 | opal (0.9.4)
98 | hike (~> 1.2)
99 | sourcemap (~> 0.1.0)
100 | sprockets (~> 3.1)
101 | tilt (>= 1.4)
102 | opal-activesupport (0.3.0)
103 | opal (>= 0.5.0, < 1.0.0)
104 | opal-browser (0.2.0)
105 | opal
106 | paggio
107 | opal-jquery (0.4.2)
108 | opal (>= 0.7.0, < 0.11.0)
109 | opal-rails (0.9.0)
110 | jquery-rails
111 | opal (>= 0.8.0, < 0.11)
112 | opal-activesupport (>= 0.0.5)
113 | opal-jquery (~> 0.4.0)
114 | rails (>= 4.0, < 6.0)
115 | sprockets-rails (< 3.0)
116 | opal-rspec (0.5.0)
117 | opal (>= 0.8.0, < 0.10)
118 | paggio (0.2.6)
119 | pkg-config (1.1.7)
120 | pry (0.10.4)
121 | coderay (~> 1.1.0)
122 | method_source (~> 0.8.1)
123 | slop (~> 3.4)
124 | pry-rails (0.3.4)
125 | pry (>= 0.9.10)
126 | pry-rescue (1.4.4)
127 | interception (>= 0.5)
128 | pry
129 | rack (1.6.4)
130 | rack-test (0.6.3)
131 | rack (>= 1.0)
132 | rails (4.2.7)
133 | actionmailer (= 4.2.7)
134 | actionpack (= 4.2.7)
135 | actionview (= 4.2.7)
136 | activejob (= 4.2.7)
137 | activemodel (= 4.2.7)
138 | activerecord (= 4.2.7)
139 | activesupport (= 4.2.7)
140 | bundler (>= 1.3.0, < 2.0)
141 | railties (= 4.2.7)
142 | sprockets-rails
143 | rails-deprecated_sanitizer (1.0.3)
144 | activesupport (>= 4.2.0.alpha)
145 | rails-dom-testing (1.0.7)
146 | activesupport (>= 4.2.0.beta, < 5.0)
147 | nokogiri (~> 1.6.0)
148 | rails-deprecated_sanitizer (>= 1.0.1)
149 | rails-html-sanitizer (1.0.3)
150 | loofah (~> 2.0)
151 | railties (4.2.7)
152 | actionpack (= 4.2.7)
153 | activesupport (= 4.2.7)
154 | rake (>= 0.8.7)
155 | thor (>= 0.18.1, < 2.0)
156 | rake (11.2.2)
157 | react-rails (1.8.1)
158 | babel-transpiler (>= 0.7.0)
159 | coffee-script-source (~> 1.8)
160 | connection_pool
161 | execjs
162 | railties (>= 3.2)
163 | tilt
164 | reactrb (0.8.8)
165 | opal (>= 0.8.0)
166 | opal-activesupport (>= 0.2.0)
167 | opal-browser (= 0.2.0)
168 | ref (2.0.0)
169 | slop (3.6.0)
170 | sourcemap (0.1.1)
171 | sprockets (3.7.0)
172 | concurrent-ruby (~> 1.0)
173 | rack (> 1, < 3)
174 | sprockets-rails (2.3.3)
175 | actionpack (>= 3.0)
176 | activesupport (>= 3.0)
177 | sprockets (>= 2.8, < 4.0)
178 | sqlite3 (1.3.11)
179 | therubyracer (0.12.2)
180 | libv8 (~> 3.16.14.0)
181 | ref
182 | thor (0.19.1)
183 | thread_safe (0.3.5)
184 | tilt (2.0.5)
185 | tzinfo (1.2.2)
186 | thread_safe (~> 0.1)
187 |
188 | PLATFORMS
189 | ruby
190 |
191 | DEPENDENCIES
192 | byebug
193 | jquery-cookie-rails
194 | opal (~> 0.9.0)
195 | opal-rails
196 | opal-rspec-rails!
197 | pry-rails
198 | pry-rescue
199 | rails
200 | react-rails
201 | reactive-record!
202 | reactrb
203 | sqlite3
204 | therubyracer
205 |
206 | BUNDLED WITH
207 | 1.12.5
208 |
--------------------------------------------------------------------------------
/lib/reactive_record/active_record/class_methods.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 |
3 | module ClassMethods
4 |
5 | def base_class
6 |
7 | unless self < Base
8 | raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
9 | end
10 |
11 | if superclass == Base || superclass.abstract_class?
12 | self
13 | else
14 | superclass.base_class
15 | end
16 |
17 | end
18 |
19 | def abstract_class?
20 | defined?(@abstract_class) && @abstract_class == true
21 | end
22 |
23 | def primary_key
24 | base_class.instance_eval { @primary_key_value || :id }
25 | end
26 |
27 | def primary_key=(val)
28 | base_class.instance_eval { @primary_key_value = val }
29 | end
30 |
31 | def inheritance_column
32 | base_class.instance_eval {@inheritance_column_value || "type"}
33 | end
34 |
35 | def inheritance_column=(name)
36 | base_class.instance_eval {@inheritance_column_value = name}
37 | end
38 |
39 | def model_name
40 | # in reality should return ActiveModel::Name object, blah blah
41 | name
42 | end
43 |
44 | def find(id)
45 | base_class.instance_eval {ReactiveRecord::Base.find(self, primary_key, id)}
46 | end
47 |
48 | def find_by(opts = {})
49 | base_class.instance_eval {ReactiveRecord::Base.find(self, opts.first.first, opts.first.last)}
50 | end
51 |
52 | def enum(*args)
53 | # when we implement schema validation we should also implement value checking
54 | end
55 |
56 | def method_missing(name, *args, &block)
57 | if args.count == 1 && name =~ /^find_by_/ && !block
58 | find_by(name.gsub(/^find_by_/, "") => args[0])
59 | else
60 | raise "#{self.name}.#{name}(#{args}) (called class method missing)"
61 | end
62 | end
63 |
64 | def abstract_class=(val)
65 | @abstract_class = val
66 | end
67 |
68 | def scope(name, body)
69 | singleton_class.send(:define_method, name) do | *args |
70 | args = (args.count == 0) ? name : [name, *args]
71 | ReactiveRecord::Base.class_scopes(self)[args] ||= ReactiveRecord::Collection.new(self, nil, nil, self, args)
72 | end
73 | singleton_class.send(:define_method, "#{name}=") do |collection|
74 | ReactiveRecord::Base.class_scopes(self)[name] = collection
75 | end
76 | end
77 |
78 | def all
79 | ReactiveRecord::Base.class_scopes(self)[:all] ||= ReactiveRecord::Collection.new(self, nil, nil, self, "all")
80 | end
81 |
82 | def all=(collection)
83 | ReactiveRecord::Base.class_scopes(self)[:all] = collection
84 | end
85 |
86 | # def server_methods(*methods)
87 | # methods.each do |method|
88 | # define_method(method) do |*args|
89 | # if args.count == 0
90 | # @backing_record.reactive_get!(method, :initialize)
91 | # else
92 | # @backing_record.reactive_get!([[method]+args], :initialize)
93 | # end
94 | # end
95 | # define_method("#{method}!") do |*args|
96 | # if args.count == 0
97 | # @backing_record.reactive_get!(method, :force)
98 | # else
99 | # @backing_record.reactive_get!([[method]+args], :force)
100 | # end
101 | # end
102 | # end
103 | # end
104 | #
105 | # alias_method :server_method, :server_methods
106 |
107 | [:belongs_to, :has_many, :has_one].each do |macro|
108 | define_method(macro) do |*args| # is this a bug in opal? saying name, scope=nil, opts={} does not work!
109 | name = args.first
110 | opts = (args.count > 1 and args.last.is_a? Hash) ? args.last : {}
111 | Associations::AssociationReflection.new(self, macro, name, opts)
112 | end
113 | end
114 |
115 | def composed_of(name, opts = {})
116 | Aggregations::AggregationReflection.new(base_class, :composed_of, name, opts)
117 | end
118 |
119 | def column_names
120 | [] # it would be great to figure out how to get this information on the client! For now we just return an empty array
121 | end
122 |
123 | [
124 | "table_name=", "before_validation", "with_options", "validates_presence_of", "validates_format_of",
125 | "accepts_nested_attributes_for", "before_create", "after_create", "before_save", "after_save", "before_destroy", "where", "validate",
126 | "attr_protected", "validates_numericality_of", "default_scope", "has_attached_file", "attr_accessible",
127 | "serialize"
128 | ].each do |method|
129 | define_method(method.to_s) { |*args, &block| }
130 | end
131 |
132 | def _react_param_conversion(param, opt = nil)
133 | # defines how react will convert incoming json to this ActiveRecord model
134 | #TIMING times = {start: Time.now.to_f, json_start: 0, json_end: 0, db_load_start: 0, db_load_end: 0}
135 | #TIMING times[:json_start] = Time.now.to_f
136 | param = Native(param)
137 | param = JSON.from_object(param.to_n) if param.is_a? Native::Object
138 | #TIMING times[:json_end] = Time.now.to_f
139 | result = if param.is_a? self
140 | param
141 | elsif param.is_a? Hash
142 | if opt == :validate_only
143 | klass = ReactiveRecord::Base.infer_type_from_hash(self, param)
144 | klass == self or klass < self
145 | else
146 | if param[primary_key]
147 | target = find(param[primary_key])
148 | else
149 | target = new
150 | end
151 | #TIMING times[:db_load_start] = Time.now.to_f
152 | ReactiveRecord::Base.load_from_json(Hash[param.collect { |key, value| [key, [value]] }], target)
153 | #TIMING times[:db_load_end] = Time.now.to_f
154 | target
155 | end
156 | else
157 | nil
158 | end
159 | #TIMING times[:end] = Time.now.to_f
160 | #TIMING puts "times - total: #{'%.04f' % (times[:end]-times[:start])}, native conversion: #{'%.04f' % (times[:json_end]-times[:json_start])}, loading: #{'%.04f' % (times[:db_load_end]-times[:db_load_start])}"
161 | result
162 | end
163 |
164 | end
165 |
166 | end
167 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/update_attributes_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "creating and updating a record" do
4 |
5 | before(:all) do
6 | React::IsomorphicHelpers.load_context
7 | end
8 |
9 | it "can create a new record" do
10 | jon = User.new({first_name: "Jon", last_name: "Weaver"})
11 | expect(jon.attributes).to eq({first_name: "Jon", last_name: "Weaver"})
12 | end
13 |
14 | it "after creating it will have no id" do
15 | jon = User.find_by_first_name("Jon")
16 | expect(jon.id).to be_nil
17 | end
18 |
19 | it "after creating it will be new" do
20 | jon = User.find_by_first_name("Jon")
21 | expect(jon).to be_new
22 | end
23 |
24 | it "after creating it will be changed" do
25 | jon = User.find_by_first_name("Jon")
26 | expect(jon.changed?).to be_truthy
27 | end
28 |
29 | it "it can calculate server side attributes before saving" do
30 | ReactiveRecord.load do
31 | User.find_by_first_name("Jon").detailed_name
32 | end.then do |name|
33 | expect(name).to eq("J. Weaver")
34 | end
35 | end
36 |
37 | it "can be saved and will have an id" do
38 | jon = User.find_by_first_name("Jon")
39 | jon.save.then { expect(jon.id).not_to be_nil }
40 | end
41 |
42 | it "can be reloaded" do
43 | React::IsomorphicHelpers.load_context
44 | ReactiveRecord.load do
45 | User.find_by_first_name("Jon").last_name
46 | end.then do |last_name|
47 | expect(last_name).to be("Weaver")
48 | end
49 | end
50 |
51 | it "will still have an id" do
52 | jon = User.find_by_first_name("Jon")
53 | expect(jon.id).not_to be_nil
54 | end
55 |
56 | it "can be updated and it will get new server side values before saving" do
57 | jon = User.find_by_last_name("Weaver")
58 | jon.email = "jonny@catprint.com"
59 | ReactiveRecord.load do
60 | jon.detailed_name
61 | end.then do |detailed_name|
62 | expect(detailed_name).to eq("J. Weaver - jonny@catprint.com")
63 | end
64 | end
65 |
66 | it "can be updated but it won't see the new server side values" do
67 | jon = User.find_by_last_name("Weaver")
68 | jon.email = "jon@catprint.com"
69 | ReactiveRecord.load do
70 | jon.detailed_name
71 | end.then do |detailed_name|
72 | expect(detailed_name).to eq("J. Weaver - jonny@catprint.com")
73 | end
74 | end
75 |
76 | it "but the bang method forces a refresh" do
77 | jon = User.find_by_last_name("Weaver")
78 | ReactiveRecord.load do
79 | jon.detailed_name! unless jon.detailed_name == "J. Weaver - jon@catprint.com"
80 | jon.detailed_name
81 | end.then do |detailed_name|
82 | expect(detailed_name).to eq("J. Weaver - jon@catprint.com")
83 | end
84 | end
85 |
86 | async "can be saved and will remember the new values" do
87 | jon = User.find_by_last_name("Weaver")
88 | jon.email = "jon@catprint.com"
89 | jon.save.then do
90 | React::IsomorphicHelpers.load_context
91 | ReactiveRecord.load do
92 | User.find_by_last_name("Weaver").email
93 | end.then do |email|
94 | async { expect(email).to be("jon@catprint.com") }
95 | end
96 | end
97 | end
98 |
99 | it "can be deleted" do
100 | jon = User.find_by_last_name("Weaver")
101 | jon.destroy.then { expect(jon.id).to be_nil }
102 | end
103 |
104 | it "does not exist in the database" do
105 | React::IsomorphicHelpers.load_context
106 | ReactiveRecord.load do
107 | User.find_by_first_name("Jon").id
108 | end.then do |id|
109 | expect(id).to be_nil
110 | end
111 | end
112 |
113 | async "it can have a one way writable attribute (might be used for a password - see the user model)" do
114 | jon = User.new({name: "Jon Weaver"})
115 | jon.save.then do
116 | React::IsomorphicHelpers.load_context
117 | ReactiveRecord.load do
118 | User.find_by_last_name("Weaver").first_name
119 | end.then do |first_name|
120 | async { expect(first_name).to be("Jon") }
121 | end
122 | end
123 | end
124 |
125 | async "load method with no block and one attribute" do
126 | React::IsomorphicHelpers.load_context
127 | user = User.find_by_email("mitch@catprint.com")
128 | user.first_name = "Robert"
129 | user.todo_items << TodoItem.new(title: "test")
130 | user.load(:detailed_name).then do |detailed_name|
131 | async do
132 | expect(detailed_name).to be("R. VanDuyn - mitch@catprint.com (3 todos)")
133 | end
134 | end
135 | end
136 |
137 | async "load method with no block and multiple attributes" do
138 | React::IsomorphicHelpers.load_context
139 | user = User.find_by_email("mitch@catprint.com")
140 | user.first_name = "Robert"
141 | user.load(:detailed_name, :name).then do |detailed_name, name|
142 | async do
143 | expect(detailed_name).to be("R. VanDuyn - mitch@catprint.com (2 todos)")
144 | expect(name).to be("Robert VanDuyn")
145 | end
146 | end
147 | end
148 |
149 | async "load method with a block and one attribute" do
150 | React::IsomorphicHelpers.load_context
151 | loop_count = 0
152 | user = User.find_by_email("mitch@catprint.com")
153 | user.first_name = "Robert"
154 | user.load(:detailed_name) do |detailed_name|
155 | loop_count += 1
156 | detailed_name
157 | end.then do |detailed_name|
158 | async do
159 | expect(loop_count).to eq(2)
160 | expect(detailed_name).to be("R. VanDuyn - mitch@catprint.com (2 todos)")
161 | end
162 | end
163 | end
164 |
165 | async "load method with a block and multiple attributes" do
166 | React::IsomorphicHelpers.load_context
167 | loop_count = 0
168 | user = User.find_by_email("mitch@catprint.com")
169 | user.first_name = "Robert"
170 | user.load(:detailed_name, :name) do |*args|
171 | loop_count += 1
172 | args
173 | end.then do |detailed_name, name|
174 | async do
175 | expect(loop_count).to eq(2)
176 | expect(detailed_name).to be("R. VanDuyn - mitch@catprint.com (2 todos)")
177 | expect(name).to be("Robert VanDuyn")
178 | end
179 | end
180 | end
181 |
182 | after(:all) do
183 | User.find_by_last_name("Weaver").destroy
184 | end
185 |
186 | end
187 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/permissions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | # for testing convienience we do an odd thing: if acting_user is nil we treat it as a super user
4 | # other wise we can provide an email which will be converted by the application controller into the acting_user
5 |
6 | describe "checking permissions" do
7 |
8 | it "will use the default permissions" do
9 | React::IsomorphicHelpers.load_context
10 | set_acting_user "super-user"
11 | ReactiveRecord.load do
12 | User.find_by_email("todd@catprint.com").id
13 | end.then do |id|
14 | expect(id).not_to be_nil
15 | end
16 | end
17 |
18 | it "will reject a view by the wrong user" do
19 | React::IsomorphicHelpers.load_context
20 | ReactiveRecord.load do |failure|
21 | if failure
22 | failure
23 | else
24 | set_acting_user "todd@catprint.com"
25 | User.find_by_email("mitch@catprint.com").id
26 | end
27 | end.then do |failure|
28 | set_acting_user "super-user"
29 | expect(failure).to include("ReactiveRecord::AccessViolation")
30 | end
31 | end
32 |
33 | it "will honor a correct view permission" do
34 | ReactiveRecord.load do
35 | set_acting_user 'mitch@catprint.com'
36 | User.find_by_first_name("Mitch").last_name
37 | end.then do |last_name|
38 | set_acting_user "super-user"
39 | expect(last_name).to eq("VanDuyn")
40 | end
41 | end
42 |
43 | it "will show data if the user is correct" do
44 | ReactiveRecord.load do
45 | set_acting_user 'mitch@catprint.com'
46 | TodoItem.find_by_title("a todo for mitch").description
47 | end.then do |description|
48 | set_acting_user "super-user"
49 | expect(description).not_to be_nil
50 | end
51 | end
52 |
53 | it "will not show data if the user is not correct" do
54 | React::IsomorphicHelpers.load_context
55 | ReactiveRecord.load do |failure|
56 | if failure
57 | failure
58 | else
59 | set_acting_user 'todd@catprint.com'
60 | TodoItem.find_by_title("a todo for mitch").description
61 | end
62 | end.then do |failure|
63 | set_acting_user "super-user"
64 | expect(failure).to include("ReactiveRecord::AccessViolation")
65 | end
66 | end
67 |
68 | it "is time to make sure mitch is loaded" do
69 | ReactiveRecord.load do
70 | User.find_by_email("mitch@catprint.com").todo_items.collect { |todo| todo.title }
71 | end.then do |todos|
72 | expect(todos).not_to be_nil
73 | end
74 | end
75 |
76 | it "is time to give mitch a new todo" do
77 | mitch = User.find_by_email("mitch@catprint.com")
78 | mitch.todo_items << TodoItem.new({title: "new todo for you"})
79 | mitch.save.then { |result| expect(result[:success]).to be_truthy }
80 | end
81 |
82 | it "should let mitch update the description" do
83 | new_todo = TodoItem.find_by_title("new todo for you")
84 | new_todo.description = "blah blah blah"
85 | set_acting_user 'mitch@catprint.com'
86 | new_todo.save.then do |result|
87 | expect(result[:success]).to be_truthy
88 | end
89 | end
90 |
91 | it "should not let somebody else update the description" do
92 | new_todo = TodoItem.find_by_title("new todo for you")
93 | new_todo.description = "I can't do this..."
94 | set_acting_user 'todd@catprint.com'
95 | new_todo.save.then do |result|
96 | expect(result[:success]).to be_falsy
97 | end
98 | end
99 |
100 | async "should let users add their own comment (tests create_permitted)" do
101 | React::IsomorphicHelpers.load_context
102 | ReactiveRecord.load do
103 | set_acting_user "super-user"
104 | TodoItem.find_by_title("new todo for you").comments.all
105 | User.find_by_email("todd@catprint.com").id
106 | end.then do
107 | new_todo = TodoItem.find_by_title("new todo for you")
108 | new_todo.comments << Comment.new({comment: "a comment", user: User.find_by_email("todd@catprint.com")})
109 | set_acting_user 'todd@catprint.com'
110 | new_todo.save.then do |result|
111 | async { expect(result[:success]).to be_truthy }
112 | end
113 | end
114 | end
115 |
116 | async "should not let a user add another user's comment (tests create_permitted)" do
117 | React::IsomorphicHelpers.load_context
118 | ReactiveRecord.load do
119 | set_acting_user "super-user"
120 | TodoItem.find_by_title("new todo for you").comments.count
121 | User.find_by_email("todd@catprint.com").id
122 | end.then do
123 | new_todo = TodoItem.find_by_title("new todo for you")
124 | new_todo.comments << Comment.new({comment: "a comment", user: User.find_by_email("todd@catprint.com")})
125 | set_acting_user 'mitch@catprint.com'
126 | new_todo.save.then do |result|
127 | async { expect(result[:success]).to be_falsy }
128 | end
129 | end
130 | end
131 |
132 | it "is time to make sure things really did work" do
133 | React::IsomorphicHelpers.load_context
134 | ReactiveRecord.load do
135 | set_acting_user "super-user"
136 | todo = TodoItem.find_by_title("new todo for you")
137 | [todo.user.first_name, todo.description, todo.comments.count, todo.comments.first.comment, todo.comments.first.user.email]
138 | end.then do |results|
139 | expect(results).to eq(["Mitch", "blah blah blah", 1, "a comment", "todd@catprint.com"])
140 | end
141 | end
142 |
143 | it "is time to delete the comment - lets see if mitch can delete it" do
144 | comment = TodoItem.find_by_title("new todo for you").comments.first
145 | set_acting_user 'mitch@catprint.com'
146 | comment.destroy.then { |result| expect(result[:success]).to be_falsy }
147 | end
148 |
149 | async "is time to delete the comment - lets do it without the promise" do
150 | React::IsomorphicHelpers.load_context
151 | comment = TodoItem.find_by_title("new todo for you").comments.first
152 | set_acting_user 'mitch@catprint.com'
153 | comment.destroy { |result, message| async { expect(result).to be_falsy; expect(message).to be_present } }
154 | end
155 |
156 | it "is time to really delete it" do
157 | React::IsomorphicHelpers.load_context
158 | set_acting_user 'todd@catprint.com'
159 | comment = Comment.find_by_comment("a comment")
160 | comment.destroy.then { |result| expect(result[:success]).to be_truthy }
161 | end
162 |
163 | it "is time to delete the todo" do
164 | set_acting_user "super-user"
165 | new_todo = TodoItem.find_by_title("new todo for you")
166 | new_todo.destroy.then { |result| expect(result[:success]).to be_truthy }
167 | end
168 |
169 | end
170 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/active_record/rendering_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | #require 'user'
3 | #require 'todo_item'
4 | #require 'address'
5 |
6 | describe "integration with react" do
7 |
8 | before(:each) { React::IsomorphicHelpers.load_context }
9 |
10 | it "find by two methods will not give the same object until loaded" do
11 | r1 = User.find_by_email("mitch@catprint.com")
12 | r2 = User.find_by_first_name("Mitch")
13 | expect(r1).not_to eq(r2)
14 | end
15 |
16 | rendering("find by two methods gives same object once loaded") do
17 | r1 = User.find_by_email("mitch@catprint.com")
18 | r2 = User.find_by_first_name("Mitch")
19 | r1.id
20 | r2.id
21 | if r1 == r2
22 | "SAME OBJECT"
23 | else
24 | "NOT YET"
25 | end
26 | end.should_generate do
27 | html == "SAME OBJECT"
28 | end
29 |
30 | it "will find two different attributes will not be equal before loading" do
31 | r1 = User.find_by_email("mitch@catprint.com")
32 | expect(r1.first_name).not_to eq(r1.last_name)
33 | end
34 |
35 | it "will find the same attributes to be equal before loading" do
36 | r1 = User.find_by_email("mitch@catprint.com")
37 | expect(r1.first_name).to eq(r1.first_name)
38 | end
39 |
40 | rendering("find by two methods gives same attributes once loaded") do
41 | r1 = User.find_by_email("mitch@catprint.com")
42 | r2 = User.find_by_first_name("Mitch")
43 | if r1.first_name == r2.first_name
44 | "SAME VALUE"
45 | else
46 | "NOT YET"
47 | end
48 | end.should_generate do
49 | html == "SAME VALUE"
50 | end
51 |
52 | it "will know that an attribute is loading" do
53 | r1 = User.find_by_email("mitch@catprint.com")
54 | expect(r1.first_name).to be_loading
55 | end
56 |
57 | rendering("an attribute will eventually set it not loading") do
58 | User.find_by_email("mitch@catprint.com").first_name.loading? ? "LOADING" : "LOADED"
59 | end.should_generate do
60 | html == "LOADED"
61 | end
62 |
63 | it "will know that an attribute is not loaded" do
64 | r1 = User.find_by_email("mitch@catprint.com")
65 | expect(r1.first_name).not_to be_loaded
66 | end
67 |
68 | rendering("an attribute will eventually set it loaded") do
69 | User.find_by_email("mitch@catprint.com").first_name.loaded? ? "LOADED" : "LOADING"
70 | end.should_generate do
71 | html == "LOADED"
72 | end
73 |
74 | it "present? returns true for a non-nil value" do
75 | expect("foo").to be_present
76 | end
77 |
78 | it "present? returns false for nil" do
79 | expect(false).not_to be_present
80 | end
81 |
82 | it "will consider a unloaded attribute not to be present" do
83 | r1 = User.find_by_email("mitch@catprint.com")
84 | expect(r1.first_name).not_to be_present
85 | end
86 |
87 | rendering("a non-nil attribute will make it present") do
88 | User.find_by_email("mitch@catprint.com").first_name.present? ? "PRESENT" : ""
89 | end.should_generate do
90 | html == "PRESENT"
91 | end
92 |
93 | rendering("a simple find_by query") do
94 | User.find_by_email("mitch@catprint.com").email
95 | end.should_immediately_generate do
96 | html == "mitch@catprint.com"
97 | end
98 |
99 | rendering("an attribute from the server") do
100 | User.find_by_email("mitch@catprint.com").first_name
101 | end.should_generate do
102 | html == "Mitch"
103 | end
104 |
105 | rendering("a has_many association") do
106 | User.find_by_email("mitch@catprint.com").todo_items.collect do |todo|
107 | todo.title
108 | end.join(", ")
109 | end.should_generate do
110 | html == "a todo for mitch, another todo for mitch"
111 | end
112 |
113 | rendering("only as many times as needed") do
114 | times_up = React::State.get_state(self, "times_up")
115 | @timer ||= after(0.5) { React::State.set_state(self, "times_up", "DONE")}
116 | @count ||= 0
117 | @count += 1
118 | "#{times_up} #{@count.to_s} " + User.find_by_email("mitch@catprint.com").todo_items.collect do |todo|
119 | todo.title
120 | end.join(", ")
121 | end.should_generate do
122 | puts "trying again: #{html}"
123 | html == "DONE 3 a todo for mitch, another todo for mitch"
124 | end
125 |
126 | rendering("a belongs_to association from id") do
127 | TodoItem.find(1).user.email
128 | end.should_generate do
129 | html == "mitch@catprint.com"
130 | end
131 |
132 | rendering("a belongs_to association from an attribute") do
133 | User.find_by_email("mitch@catprint.com").todo_items.first.user.email
134 | end.should_generate do
135 | html == "mitch@catprint.com"
136 | end
137 |
138 | rendering("an aggregation") do
139 | User.find_by_email("mitch@catprint.com").address.city
140 | end.should_generate do
141 | html == "Rochester"
142 | end
143 |
144 | rendering("a record that is updated multiple times") do
145 | unless @record
146 | @record = User.new
147 | @record.attributes[:all_done] = false
148 | @record.attributes[:test_done] = false
149 | @record.attributes[:counter] = 0
150 | end
151 | puts "rendering #{@record} #{@record.attributes[:counter]}"
152 | after(0.1) do
153 | puts "update counter timer expired, @record.test_done = #{!!@record.test_done}"
154 | @record.counter = @record.counter + 1 unless @record.test_done
155 | end
156 | puts "record.changed? #{!!@record.changed?}"
157 | after(2) do
158 | puts "all done timer expired test should get done now!"
159 | @record.all_done = true
160 | end unless @record.changed?
161 | if @record.all_done
162 | @record.all_done = nil
163 | @record.test_done = true
164 | "#{@record.counter}"
165 | else
166 | "not done yet... #{@record.changed?}, #{@record.attributes[:counter]}"
167 | end
168 | end.should_generate do
169 | puts "html = #{html}"
170 | html == "2"
171 | end
172 |
173 | rendering("changing an aggregate is noticed by the parent") do
174 | @user ||= User.find_by_email("mitch@catprint.com")
175 | after(0.1) do
176 | @user.address.city = "Timbuktoo"
177 | end
178 | if @user.changed?
179 | "#{@user.address.city}"
180 | end
181 | end.should_generate do
182 | html == "Timbuktoo"
183 | end
184 |
185 | rendering("a server side value dynamically changed before first fetch from server") do
186 | puts "rendering"
187 | @update ||= after(0.001) do
188 | puts "async update"
189 | mitch = User.find_by_email("mitch@catprint.com")
190 | mitch.first_name = "Robert"
191 | mitch.detailed_name!
192 | puts "updated"
193 | end
194 | User.find_by_email("mitch@catprint.com").detailed_name
195 | end.should_generate do
196 | puts "html = #{html}"
197 | html == "R. VanDuyn - mitch@catprint.com (2 todos)"
198 | end
199 |
200 | rendering("a server side value dynamically changed after first fetch from server") do
201 | puts "rendering"
202 | @update ||= after(1) do
203 | puts "async update"
204 | mitch = User.find_by_email("mitch@catprint.com")
205 | mitch.first_name = "Robert"
206 | mitch.detailed_name!
207 | puts "updated"
208 | end
209 | User.find_by_email("mitch@catprint.com").detailed_name
210 | end.should_generate do
211 | puts "html = #{html}"
212 | html == "R. VanDuyn - mitch@catprint.com (2 todos)"
213 | end
214 |
215 | end
216 |
--------------------------------------------------------------------------------
/lib/reactive_record/active_record/reactive_record/collection.rb:
--------------------------------------------------------------------------------
1 | module ReactiveRecord
2 |
3 | class Collection
4 |
5 | def initialize(target_klass, owner = nil, association = nil, *vector)
6 | @owner = owner # can be nil if this is an outer most scope
7 | @association = association
8 | @target_klass = target_klass
9 | if owner and !owner.id and vector.length <= 1
10 | @collection = []
11 | elsif vector.length > 0
12 | @vector = vector
13 | elsif owner
14 | @vector = owner.backing_record.vector + [association.attribute]
15 | else
16 | @vector = [target_klass]
17 | end
18 | @scopes = {}
19 | end
20 |
21 | def dup_for_sync
22 | self.dup.instance_eval do
23 | @collection = @collection.dup if @collection
24 | @scopes = @scopes.dup
25 | self
26 | end
27 | end
28 |
29 | def all
30 | observed
31 | @dummy_collection.notify if @dummy_collection
32 | unless @collection
33 | @collection = []
34 | if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
35 | ids.each do |id|
36 | @collection << @target_klass.find_by(@target_klass.primary_key => id)
37 | end
38 | else
39 | @dummy_collection = ReactiveRecord::Base.load_from_db(nil, *@vector, "*all")
40 | @dummy_record = self[0]
41 | end
42 | end
43 | @collection
44 | end
45 |
46 | def [](index)
47 | observed
48 | if (@collection || all).length <= index and @dummy_collection
49 | (@collection.length..index).each do |i|
50 | new_dummy_record = ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*#{i}")
51 | new_dummy_record.backing_record.attributes[@association.inverse_of] = @owner if @association and @association.inverse_of
52 | @collection << new_dummy_record
53 | end
54 | end
55 | @collection[index]
56 | end
57 |
58 | def ==(other_collection)
59 | observed
60 | return !@collection unless other_collection.is_a? Collection
61 | other_collection.observed
62 | my_collection = (@collection || []).select { |target| target != @dummy_record }
63 | other_collection = (other_collection ? (other_collection.collection || []) : []).select { |target| target != other_collection.dummy_record }
64 | my_collection == other_collection
65 | end
66 |
67 | def apply_scope(scope, *args)
68 | # The value returned is another ReactiveRecordCollection with the scope added to the vector
69 | # no additional action is taken
70 | scope = [scope, *args] if args.count > 0
71 | @scopes[scope] ||= Collection.new(@target_klass, @owner, @association, *@vector, [scope])
72 | end
73 |
74 | def count
75 | observed
76 | if @collection
77 | @collection.count
78 | elsif @count ||= ReactiveRecord::Base.fetch_from_db([*@vector, "*count"])
79 | @count
80 | else
81 | ReactiveRecord::Base.load_from_db(nil, *@vector, "*count")
82 | @count = 1
83 | end
84 | end
85 |
86 | alias_method :length, :count
87 |
88 | def proxy_association
89 | @association || self # returning self allows this to work with things like Model.all
90 | end
91 |
92 | def klass
93 | @target_klass
94 | end
95 |
96 | def <<(item)
97 | return delete(item) if item.destroyed? # pushing a destroyed item is the same as removing it
98 | backing_record = item.backing_record
99 | all << item unless all.include? item # does this use == if so we are okay...
100 | if backing_record and @owner and @association and inverse_of = @association.inverse_of and item.attributes[inverse_of] != @owner
101 | current_association = item.attributes[inverse_of]
102 | backing_record.virgin = false unless backing_record.data_loading?
103 | backing_record.update_attribute(inverse_of, @owner)
104 | current_association.attributes[@association.attribute].delete(item) if current_association and current_association.attributes[@association.attribute]
105 | @owner.backing_record.update_attribute(@association.attribute) # forces a check if association contents have changed from synced values
106 | end
107 | if item.id and @dummy_record
108 | @dummy_record.id = item.id
109 | @collection.delete(@dummy_record)
110 | @dummy_record = @collection.detect { |r| r.backing_record.vector.last =~ /^\*[0-9]+$/ }
111 | @dummy_collection = nil
112 | end
113 | notify_of_change self
114 | end
115 |
116 | [:first, :last].each do |method|
117 | define_method method do |*args|
118 | if args.count == 0
119 | all.send(method)
120 | else
121 | apply_scope(method, *args)
122 | end
123 | end
124 | end
125 |
126 | def replace(new_array)
127 |
128 | # not tested if you do all[n] where n > 0... this will create additional dummy items, that this will not sync up.
129 | # probably just moving things around so the @dummy_collection and @dummy_record are updated AFTER the new items are pushed
130 | # should work.
131 |
132 | if @dummy_collection
133 | @dummy_collection.notify
134 | array = new_array.is_a?(Collection) ? new_array.collection : new_array
135 | @collection.each_with_index do |r, i|
136 | r.id = new_array[i].id if array[i] and array[i].id and !r.new? and r.backing_record.vector.last =~ /^\*[0-9]+$/
137 | end
138 | end
139 |
140 | @collection.dup.each { |item| delete(item) } if @collection # this line is a big nop I think
141 | @collection = []
142 | if new_array.is_a? Collection
143 | @dummy_collection = new_array.dummy_collection
144 | @dummy_record = new_array.dummy_record
145 | new_array.collection.each { |item| self << item } if new_array.collection
146 | else
147 | @dummy_collection = @dummy_record = nil
148 | new_array.each { |item| self << item }
149 | end
150 | notify_of_change new_array
151 | end
152 |
153 | def delete(item)
154 | notify_of_change(if @owner and @association and inverse_of = @association.inverse_of
155 | if backing_record = item.backing_record and backing_record.attributes[inverse_of] == @owner
156 | # the if prevents double update if delete is being called from << (see << above)
157 | backing_record.update_attribute(inverse_of, nil)
158 | end
159 | all.delete(item).tap { @owner.backing_record.update_attribute(@association.attribute) } # forces a check if association contents have changed from synced values
160 | else
161 | all.delete(item)
162 | end)
163 | end
164 |
165 | def loading?
166 | all # need to force initialization at this point
167 | @dummy_collection.loading?
168 | end
169 |
170 | def empty? # should be handled by method missing below, but opal-rspec does not deal well with method missing, so to test...
171 | all.empty?
172 | end
173 |
174 | def method_missing(method, *args, &block)
175 | if [].respond_to? method
176 | all.send(method, *args, &block)
177 | elsif @target_klass.respond_to?(method) or (args.count == 1 && method =~ /^find_by_/)
178 | apply_scope(method, *args)
179 | else
180 | super
181 | end
182 | end
183 |
184 | protected
185 |
186 | def dummy_record
187 | @dummy_record
188 | end
189 |
190 | def collection
191 | @collection
192 | end
193 |
194 | def dummy_collection
195 | @dummy_collection
196 | end
197 |
198 | def notify_of_change(value = nil)
199 | React::State.set_state(self, "collection", collection) unless ReactiveRecord::Base.data_loading?
200 | value
201 | end
202 |
203 | def observed
204 | React::State.get_state(self, "collection") unless ReactiveRecord::Base.data_loading?
205 | end
206 |
207 | end
208 |
209 | end
210 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/spec_helper.js.rb:
--------------------------------------------------------------------------------
1 | require 'opal'
2 | require 'opal-rspec'
3 | require 'reactive_record_config'
4 | require 'react' #_js_test_only'
5 | require 'reactrb'
6 | #require 'reactrb/deep-compare'
7 | require 'reactive-record'
8 | require 'jquery'
9 | require 'opal-jquery'
10 | require 'jquery.cookie'
11 | require 'models'
12 |
13 |
14 | Document.ready? do
15 | `$.cookie('acting_user', null, { path: '/' })`
16 | Opal::RSpec::Runner.autorun rescue nil
17 | end
18 |
19 | def sequenced_asyncs?
20 | return true
21 | #Opal::RSpec::Runner.method_defined?(auto_run)
22 | #ruby_version = RUBY_ENGINE_VERSION.split(".")
23 | #ruby_version[0].to_i > 0 or ruby_version[1].to_i > 8
24 | end
25 |
26 | class Object
27 |
28 | def use_case(*args, &block)
29 | describe(*args) do
30 | clear_opal_rspec_runners
31 | instance_eval &block
32 | last_promise = nil
33 | it "test starting" do
34 | expect(true).to be_truthy
35 | end
36 | runners = opal_rspec_runners
37 | runners = runners.reverse unless sequenced_asyncs?
38 | runners.each do |type, title, opts, block, promise|
39 | promise_to_resolve = last_promise
40 | async(title, opts) do
41 | promise.then do
42 | message = " running #{type.gsub('_',' ')} #{title}"
43 | `console.warn(#{message})`
44 | Opal::RSpec::AsyncHelpers::ClassMethods.set_current_promise self, promise_to_resolve
45 | begin
46 | instance_eval &block if block
47 | rescue Exception => e
48 | message = "Failed to run #{type} #{title}\nTest raised exception before starting test block: #{e}"
49 | `console.error(#{message})`
50 | end
51 | end
52 | end
53 | last_promise = promise
54 | last_promise.resolve if sequenced_asyncs?
55 | end
56 | last_promise.resolve unless sequenced_asyncs?
57 | end
58 | end
59 |
60 | end
61 |
62 | module Opal
63 | module RSpec
64 | module AsyncHelpers
65 | module ClassMethods
66 |
67 | def self.set_current_promise(instance, promise)
68 | @current_promise = promise
69 | @current_promise_test_instance = instance
70 | end
71 |
72 | def self.resolve_current_promise
73 | @current_promise.resolve if !sequenced_asyncs? && @current_promise
74 | rescue Exception => e
75 | raise "test structure error: Usually this is caused by a use_case test that has only a first_it an no other tests. Check the use_case that ran just before this one."
76 | end
77 |
78 | def self.get_current_promise_test_instance
79 | @current_promise_test_instance
80 | end
81 |
82 | #alias_method :old_it, :it
83 |
84 | #def it(*args, &block)
85 | # @previous_promise = new_promise
86 | # old_it(*args, &block)
87 | #end
88 |
89 | def opal_rspec_runners
90 | @opal_rspec_runners
91 | end
92 |
93 | def clear_opal_rspec_runners
94 | @opal_rspec_runners = []
95 | end
96 |
97 | def opal_rspec_push_runner(type, title, opts, block)
98 | @opal_rspec_runners << [type, title, opts, block, Promise.new]
99 | end
100 |
101 |
102 | def first_it(title, opts = {}, &block)
103 | opal_rspec_push_runner("first_it", title, opts, block)
104 | end
105 |
106 | def now_it(title, opts = {}, &block)
107 | opal_rspec_push_runner("now_it", title, opts, block)
108 | end
109 |
110 | def and_it(title, opts = {}, &block)
111 | opal_rspec_push_runner("and_it", title, opts, block)
112 | end
113 |
114 | def finally(title, opts = {}, &block)
115 | opal_rspec_push_runner("finally", title, opts, block)
116 | end
117 |
118 | def rendering(title, &block)
119 | klass = Class.new do
120 |
121 | include React::Component
122 |
123 | def self.block
124 | @block
125 | end
126 |
127 | def self.name
128 | "dummy class"
129 | end
130 |
131 | backtrace :on
132 |
133 | def render
134 | instance_eval &self.class.block
135 | end
136 |
137 | def self.should_generate(opts={}, &block)
138 | sself = self
139 | @self.async(@title, opts) do
140 | expect_component_to_eventually(sself, &block)
141 | end
142 | end
143 |
144 | def self.should_immediately_generate(opts={}, &block)
145 | sself = self
146 | @self.it(@title, opts) do
147 | element = build_element sself, {}
148 | context = block.arity > 0 ? self : element
149 | expect((element and context.instance_exec(element, &block))).to be(true)
150 | end
151 | end
152 |
153 | end
154 | klass.instance_variable_set("@block", block)
155 | klass.instance_variable_set("@self", self)
156 | klass.instance_variable_set("@title", "it can render #{title}")
157 | klass
158 | end
159 | end
160 | end
161 | end
162 | end
163 |
164 | module ReactTestHelpers
165 |
166 | `var ReactTestUtils = React.addons.TestUtils`
167 |
168 | def renderToDocument(type, options = {})
169 | element = React.create_element(type, options)
170 | return renderElementToDocument(element)
171 | end
172 |
173 | def renderElementToDocument(element)
174 | instance = Native(`ReactTestUtils.renderIntoDocument(#{element.to_n})`)
175 | instance.class.include(React::Component::API)
176 | return instance
177 | end
178 |
179 | def simulateEvent(event, element, params = {})
180 | simulator = Native(`ReactTestUtils.Simulate`)
181 | #element = `#{element.to_n}.getDOMNode` unless element.class == Element
182 | simulator[event.to_s].call(element.dom_node, params)
183 | #simulator[event.to_s].call(element, params)
184 | end
185 |
186 | def isElementOfType(element, type)
187 | `React.addons.TestUtils.isElementOfType(#{element.to_n}, #{type.cached_component_class})`
188 | end
189 |
190 | def build_element(type, options)
191 | component = React.create_element(type, options)
192 | element = `ReactTestUtils.renderIntoDocument(#{component.to_n})`
193 |
194 | if !(`typeof ReactDOM === 'undefined' || typeof ReactDOM.findDOMNode === 'undefined'`)
195 | `$(ReactDOM.findDOMNode(element))` # v0.14.0
196 | elsif !(`typeof React.findDOMNode === 'undefined'`)
197 | `$(React.findDOMNode(element))` # v0.13.0
198 | else
199 | `$(element.getDOMNode())` # v0.12.0
200 | end
201 | end
202 |
203 | def expect_component_to_eventually(component_class, opts = {}, &block)
204 | # Calls block after each update of a component until it returns true. When it does set the expectation to true.
205 | # Uses the after_update callback of the component_class, then instantiates an element of that class
206 | # The call back is only called on updates, so the call back is manually called right after the
207 | # element is created.
208 | # Because React.rb runs the callback inside the components context, we have to
209 | # setup a lambda to get back to correct context before executing run_async.
210 | # Because run_async can only be run once it is protected by clearing element once the test passes.
211 | element = nil
212 | check_block = lambda do
213 | context = block.arity > 0 ? self : element
214 | run_async do
215 | element = nil; expect(true).to be(true)
216 | end if element and context.instance_exec(element, &block)
217 | end
218 | component_class.after_update { check_block.call }
219 | element = build_element component_class, opts
220 | check_block.call
221 | end
222 |
223 | def test(&block)
224 | Promise.new.tap do |promise|
225 | promise.then_test &block
226 | promise.resolve
227 | end
228 | end
229 |
230 | # for the permissions test
231 |
232 | def set_acting_user(email)
233 | `$.cookie('acting_user', #{email}, { path: '/' })`
234 | end
235 |
236 | end
237 |
238 | class Promise
239 |
240 | def then_test(&block)
241 | self.then do |args|
242 | Opal::RSpec::AsyncHelpers::ClassMethods.get_current_promise_test_instance.run_async do
243 | yield args
244 | Opal::RSpec::AsyncHelpers::ClassMethods.resolve_current_promise
245 | end
246 | end
247 | end
248 |
249 | def while_waiting(&block)
250 | if sequenced_asyncs?
251 | self.then_test {Opal::RSpec::AsyncHelpers::ClassMethods.get_current_promise_test_instance.run_async { expect(true).to be_truthy }}
252 | block.call
253 | else
254 | self.then do
255 | Opal::RSpec::AsyncHelpers::ClassMethods.resolve_current_promise
256 | end
257 | Opal::RSpec::AsyncHelpers::ClassMethods.get_current_promise_test_instance.run_async &block
258 | end
259 | end
260 |
261 | end
262 |
263 | RSpec.configure do |config|
264 | config.run_all_when_everything_filtered = true
265 | config.filter_run_including only: true
266 | config.include ReactTestHelpers
267 | config.before(:each) do
268 | `current_state = {}`
269 | end
270 | end
271 |
--------------------------------------------------------------------------------
/spec/test_app/README.rdoc:
--------------------------------------------------------------------------------
1 | == Welcome to Rails
2 |
3 | Rails is a web-application framework that includes everything needed to create
4 | database-backed web applications according to the Model-View-Control pattern.
5 |
6 | This pattern splits the view (also called the presentation) into "dumb"
7 | templates that are primarily responsible for inserting pre-built data in between
8 | HTML tags. The model contains the "smart" domain objects (such as Account,
9 | Product, Person, Post) that holds all the business logic and knows how to
10 | persist themselves to a database. The controller handles the incoming requests
11 | (such as Save New Account, Update Product, Show Post) by manipulating the model
12 | and directing data to the view.
13 |
14 | In Rails, the model is handled by what's called an object-relational mapping
15 | layer entitled Active Record. This layer allows you to present the data from
16 | database rows as objects and embellish these data objects with business logic
17 | methods. You can read more about Active Record in
18 | link:files/vendor/rails/activerecord/README.html.
19 |
20 | The controller and view are handled by the Action Pack, which handles both
21 | layers by its two parts: Action View and Action Controller. These two layers
22 | are bundled in a single package due to their heavy interdependence. This is
23 | unlike the relationship between the Active Record and Action Pack that is much
24 | more separate. Each of these packages can be used independently outside of
25 | Rails. You can read more about Action Pack in
26 | link:files/vendor/rails/actionpack/README.html.
27 |
28 |
29 | == Getting Started
30 |
31 | 1. At the command prompt, create a new Rails application:
32 | rails new myapp (where myapp is the application name)
33 |
34 | 2. Change directory to myapp and start the web server:
35 | cd myapp; rails server (run with --help for options)
36 |
37 | 3. Go to http://localhost:3000/ and you'll see:
38 | "Welcome aboard: You're riding Ruby on Rails!"
39 |
40 | 4. Follow the guidelines to start developing your application. You can find
41 | the following resources handy:
42 |
43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html
44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/
45 |
46 |
47 | == Debugging Rails
48 |
49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that
50 | will help you debug it and get it back on the rails.
51 |
52 | First area to check is the application log files. Have "tail -f" commands
53 | running on the server.log and development.log. Rails will automatically display
54 | debugging and runtime information to these files. Debugging info will also be
55 | shown in the browser on requests from 127.0.0.1.
56 |
57 | You can also log your own messages directly into the log file from your code
58 | using the Ruby logger class from inside your controllers. Example:
59 |
60 | class WeblogController < ActionController::Base
61 | def destroy
62 | @weblog = Weblog.find(params[:id])
63 | @weblog.destroy
64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!")
65 | end
66 | end
67 |
68 | The result will be a message in your log file along the lines of:
69 |
70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1!
71 |
72 | More information on how to use the logger is at http://www.ruby-doc.org/core/
73 |
74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are
75 | several books available online as well:
76 |
77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe)
78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide)
79 |
80 | These two books will bring you up to speed on the Ruby language and also on
81 | programming in general.
82 |
83 |
84 | == Debugger
85 |
86 | Debugger support is available through the debugger command when you start your
87 | Mongrel or WEBrick server with --debugger. This means that you can break out of
88 | execution at any point in the code, investigate and change the model, and then,
89 | resume execution! You need to install ruby-debug to run the server in debugging
90 | mode. With gems, use sudo gem install ruby-debug. Example:
91 |
92 | class WeblogController < ActionController::Base
93 | def index
94 | @posts = Post.all
95 | debugger
96 | end
97 | end
98 |
99 | So the controller will accept the action, run the first line, then present you
100 | with a IRB prompt in the server window. Here you can do things like:
101 |
102 | >> @posts.inspect
103 | => "[#nil, "body"=>nil, "id"=>"1"}>,
105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]"
107 | >> @posts.first.title = "hello from a debugger"
108 | => "hello from a debugger"
109 |
110 | ...and even better, you can examine how your runtime objects actually work:
111 |
112 | >> f = @posts.first
113 | => #nil, "body"=>nil, "id"=>"1"}>
114 | >> f.
115 | Display all 152 possibilities? (y or n)
116 |
117 | Finally, when you're ready to resume execution, you can enter "cont".
118 |
119 |
120 | == Console
121 |
122 | The console is a Ruby shell, which allows you to interact with your
123 | application's domain model. Here you'll have all parts of the application
124 | configured, just like it is when the application is running. You can inspect
125 | domain models, change values, and save to the database. Starting the script
126 | without arguments will launch it in the development environment.
127 |
128 | To start the console, run rails console from the application
129 | directory.
130 |
131 | Options:
132 |
133 | * Passing the -s, --sandbox argument will rollback any modifications
134 | made to the database.
135 | * Passing an environment name as an argument will load the corresponding
136 | environment. Example: rails console production.
137 |
138 | To reload your controllers and models after launching the console run
139 | reload!
140 |
141 | More information about irb can be found at:
142 | link:http://www.rubycentral.org/pickaxe/irb.html
143 |
144 |
145 | == dbconsole
146 |
147 | You can go to the command line of your database directly through rails
148 | dbconsole. You would be connected to the database with the credentials
149 | defined in database.yml. Starting the script without arguments will connect you
150 | to the development database. Passing an argument will connect you to a different
151 | database, like rails dbconsole production. Currently works for MySQL,
152 | PostgreSQL and SQLite 3.
153 |
154 | == Description of Contents
155 |
156 | The default directory structure of a generated Ruby on Rails application:
157 |
158 | |-- app
159 | | |-- assets
160 | | | |-- images
161 | | | |-- javascripts
162 | | | `-- stylesheets
163 | | |-- controllers
164 | | |-- helpers
165 | | |-- mailers
166 | | |-- models
167 | | `-- views
168 | | `-- layouts
169 | |-- config
170 | | |-- environments
171 | | |-- initializers
172 | | `-- locales
173 | |-- db
174 | |-- doc
175 | |-- lib
176 | | |-- assets
177 | | `-- tasks
178 | |-- log
179 | |-- public
180 | |-- script
181 | |-- test
182 | | |-- fixtures
183 | | |-- functional
184 | | |-- integration
185 | | |-- performance
186 | | `-- unit
187 | |-- tmp
188 | | `-- cache
189 | | `-- assets
190 | `-- vendor
191 | |-- assets
192 | | |-- javascripts
193 | | `-- stylesheets
194 | `-- plugins
195 |
196 | app
197 | Holds all the code that's specific to this particular application.
198 |
199 | app/assets
200 | Contains subdirectories for images, stylesheets, and JavaScript files.
201 |
202 | app/controllers
203 | Holds controllers that should be named like weblogs_controller.rb for
204 | automated URL mapping. All controllers should descend from
205 | ApplicationController which itself descends from ActionController::Base.
206 |
207 | app/models
208 | Holds models that should be named like post.rb. Models descend from
209 | ActiveRecord::Base by default.
210 |
211 | app/views
212 | Holds the template files for the view that should be named like
213 | weblogs/index.html.erb for the WeblogsController#index action. All views use
214 | eRuby syntax by default.
215 |
216 | app/views/layouts
217 | Holds the template files for layouts to be used with views. This models the
218 | common header/footer method of wrapping views. In your views, define a layout
219 | using the layout :default and create a file named default.html.erb.
220 | Inside default.html.erb, call <% yield %> to render the view using this
221 | layout.
222 |
223 | app/helpers
224 | Holds view helpers that should be named like weblogs_helper.rb. These are
225 | generated for you automatically when using generators for controllers.
226 | Helpers can be used to wrap functionality for your views into methods.
227 |
228 | config
229 | Configuration files for the Rails environment, the routing map, the database,
230 | and other dependencies.
231 |
232 | db
233 | Contains the database schema in schema.rb. db/migrate contains all the
234 | sequence of Migrations for your schema.
235 |
236 | doc
237 | This directory is where your application documentation will be stored when
238 | generated using rake doc:app
239 |
240 | lib
241 | Application specific libraries. Basically, any kind of custom code that
242 | doesn't belong under controllers, models, or helpers. This directory is in
243 | the load path.
244 |
245 | public
246 | The directory available for the web server. Also contains the dispatchers and the
247 | default HTML files. This should be set as the DOCUMENT_ROOT of your web
248 | server.
249 |
250 | script
251 | Helper scripts for automation and generation.
252 |
253 | test
254 | Unit and functional tests along with fixtures. When using the rails generate
255 | command, template test files will be generated for you and placed in this
256 | directory.
257 |
258 | vendor
259 | External libraries that the application depends on. Also includes the plugins
260 | subdirectory. If the app has frozen rails, those gems also go here, under
261 | vendor/rails/. This directory is in the load path.
262 |
--------------------------------------------------------------------------------
/lib/reactive_record/active_record/reactive_record/while_loading.rb:
--------------------------------------------------------------------------------
1 | module ReactiveRecord
2 |
3 | # will repeatedly execute the block until it is loaded
4 | # immediately returns a promise that will resolve once the block is loaded
5 |
6 | def self.load(&block)
7 | promise = Promise.new
8 | @load_stack ||= []
9 | @load_stack << @loads_pending
10 | @loads_pending = nil
11 | result = block.call
12 | if @loads_pending
13 | @blocks_to_load ||= []
14 | @blocks_to_load << [Base.last_fetch_at, promise, block]
15 | else
16 | promise.resolve result
17 | end
18 | @loads_pending = @load_stack.pop
19 | promise
20 | rescue Exception => e
21 | React::IsomorphicHelpers.log "ReactiveRecord.load exception raised during initial load: #{e}", :error
22 | end
23 |
24 | def self.loads_pending!
25 | @loads_pending = true
26 | end
27 |
28 | def self.check_loads_pending
29 | if @loads_pending
30 | if Base.pending_fetches.count > 0
31 | true
32 | else # this happens when for example loading foo.x results in somebody looking at foo.y while foo.y is still being loaded
33 | ReactiveRecord::WhileLoading.loaded_at Base.last_fetch_at
34 | ReactiveRecord::WhileLoading.quiet!
35 | false
36 | end
37 | end
38 | end
39 |
40 | def self.run_blocks_to_load(fetch_id, failure = nil)
41 | if @blocks_to_load
42 | blocks_to_load_now = @blocks_to_load.select { |data| data.first == fetch_id }
43 | @blocks_to_load = @blocks_to_load.reject { |data| data.first == fetch_id }
44 | @load_stack ||= []
45 | blocks_to_load_now.each do |data|
46 | id, promise, block = data
47 | @load_stack << @loads_pending
48 | @loads_pending = nil
49 | result = block.call(failure)
50 | if check_loads_pending and !failure
51 | @blocks_to_load << [Base.last_fetch_at, promise, block]
52 | else
53 | promise.resolve result
54 | end
55 | @loads_pending = @load_stack.pop
56 | end
57 | end
58 | rescue Exception => e
59 | React::IsomorphicHelpers.log "ReactiveRecord.load exception raised during retry: #{e}", :error
60 | end
61 |
62 |
63 | # Adds while_loading feature to React
64 | # to use attach a .while_loading handler to any element for example
65 | # div { "displayed if everything is loaded" }.while_loading { "displayed while I'm loading" }
66 | # the contents of the div will be switched (using jQuery.show/hide) depending on the state of contents of the first block
67 |
68 | # To notify React that something is loading use React::WhileLoading.loading!
69 | # once everything is loaded then do React::WhileLoading.loaded_at message (typically a time stamp just for debug purposes)
70 |
71 | class WhileLoading
72 |
73 | include React::IsomorphicHelpers
74 |
75 | before_first_mount do
76 | @css_to_preload = ""
77 | @while_loading_counter = 0
78 | end
79 |
80 | def get_next_while_loading_counter
81 | @while_loading_counter += 1
82 | end
83 |
84 | def preload_css(css)
85 | @css_to_preload << css << "\n"
86 | end
87 |
88 | prerender_footer do
89 | "".tap { @css_to_preload = ""}
90 | end
91 |
92 | if RUBY_ENGINE == 'opal'
93 |
94 | # I DONT THINK WE USE opal-jquery in this module anymore - require 'opal-jquery' if opal_client?
95 |
96 | include React::Component
97 |
98 | param :loading
99 | param :loaded_children
100 | param :loading_children
101 | param :element_type
102 | param :element_props
103 | param :display, default: ""
104 |
105 | class << self
106 |
107 | def loading?
108 | @is_loading
109 | end
110 |
111 | def loading!
112 | React::RenderingContext.waiting_on_resources = true
113 | React::State.get_state(self, :loaded_at)
114 | React::State.set_state(self, :quiet, false)
115 | @is_loading = true
116 | end
117 |
118 | def loaded_at(loaded_at)
119 | React::State.set_state(self, :loaded_at, loaded_at)
120 | @is_loading = false
121 | end
122 |
123 | def quiet?
124 | React::State.get_state(self, :quiet)
125 | end
126 |
127 | def page_loaded?
128 | React::State.get_state(self, :page_loaded)
129 | end
130 |
131 | def quiet!
132 | React::State.set_state(self, :quiet, true)
133 | after(1) { React::State.set_state(self, :page_loaded, true) } unless on_opal_server? or @page_loaded
134 | @page_loaded = true
135 | end
136 |
137 | def add_style_sheet
138 | @style_sheet ||= %x{
139 | $('').appendTo("head")
143 | }
144 | end
145 |
146 | end
147 |
148 | before_mount do
149 | @uniq_id = WhileLoading.get_next_while_loading_counter
150 | WhileLoading.preload_css(
151 | ".reactive_record_while_loading_container_#{@uniq_id} > :nth-child(1n+#{loaded_children.count+1}) {\n"+
152 | " display: none;\n"+
153 | "}\n"
154 | )
155 | end
156 |
157 | after_mount do
158 | @waiting_on_resources = loading
159 | WhileLoading.add_style_sheet
160 | %x{
161 | var node = #{dom_node};
162 | $(node).children(':nth-child(-1n+'+#{loaded_children.count}+')').addClass('reactive_record_show_when_loaded');
163 | $(node).children(':nth-child(1n+'+#{loaded_children.count+1}+')').addClass('reactive_record_show_while_loading');
164 | }
165 | end
166 |
167 | after_update do
168 | @waiting_on_resources = loading
169 | end
170 |
171 | def render
172 | props = element_props.dup
173 | classes = [props[:class], props[:className], "reactive_record_while_loading_container_#{@uniq_id}"].compact.join(" ")
174 | props.merge!({
175 | "data-reactive_record_while_loading_container_id" => @uniq_id,
176 | "data-reactive_record_enclosing_while_loading_container_id" => @uniq_id,
177 | class: classes
178 | })
179 | React.create_element(element_type, props) { loaded_children + loading_children }
180 | end
181 |
182 | end
183 |
184 | end
185 |
186 | end
187 |
188 | module React
189 |
190 | class Element
191 |
192 | def while_loading(display = "", &loading_display_block)
193 |
194 | loaded_children = []
195 | loaded_children = block.call.dup if block
196 |
197 | loading_children = [display]
198 | loading_children = RenderingContext.build do |buffer|
199 | result = loading_display_block.call
200 | buffer << result.to_s if result.is_a? String
201 | buffer.dup
202 | end if loading_display_block
203 | RenderingContext.replace(
204 | self,
205 | React.create_element(
206 | ReactiveRecord::WhileLoading,
207 | loading: waiting_on_resources,
208 | loading_children: loading_children,
209 | loaded_children: loaded_children,
210 | element_type: type,
211 | element_props: properties)
212 | )
213 | end
214 |
215 | def hide_while_loading
216 | while_loading
217 | end
218 |
219 | end
220 |
221 | module Component
222 |
223 | alias_method :original_component_did_mount, :component_did_mount
224 |
225 | def component_did_mount(*args)
226 | original_component_did_mount(*args)
227 | reactive_record_link_to_enclosing_while_loading_container
228 | reactive_record_link_set_while_loading_container_class
229 | end
230 |
231 | alias_method :original_component_did_update, :component_did_update
232 |
233 | def component_did_update(*args)
234 | original_component_did_update(*args)
235 | reactive_record_link_set_while_loading_container_class
236 | end
237 |
238 | def reactive_record_link_to_enclosing_while_loading_container
239 | # Call after any component mounts - attaches the containers loading id to this component
240 | # Fyi, the while_loading container is responsible for setting its own link to itself
241 |
242 | %x{
243 | var node = #{dom_node};
244 | if (!$(node).is('[data-reactive_record_enclosing_while_loading_container_id]')) {
245 | var while_loading_container = $(node).closest('[data-reactive_record_while_loading_container_id]')
246 | if (while_loading_container.length > 0) {
247 | var container_id = $(while_loading_container).attr('data-reactive_record_while_loading_container_id')
248 | $(node).attr('data-reactive_record_enclosing_while_loading_container_id', container_id)
249 | }
250 | }
251 | }
252 |
253 | end
254 |
255 | def reactive_record_link_set_while_loading_container_class
256 |
257 | %x{
258 |
259 | var node = #{dom_node};
260 | var while_loading_container_id = $(node).attr('data-reactive_record_enclosing_while_loading_container_id');
261 | if (while_loading_container_id) {
262 | var while_loading_container = $('[data-reactive_record_while_loading_container_id='+while_loading_container_id+']');
263 | var loading = (#{waiting_on_resources} == true);
264 | if (loading) {
265 | $(node).addClass('reactive_record_is_loading');
266 | $(node).removeClass('reactive_record_is_loaded');
267 | $(while_loading_container).addClass('reactive_record_is_loading');
268 | $(while_loading_container).removeClass('reactive_record_is_loaded');
269 |
270 | } else if (!$(node).hasClass('reactive_record_is_loaded')) {
271 |
272 | if (!$(node).attr('data-reactive_record_while_loading_container_id')) {
273 | $(node).removeClass('reactive_record_is_loading');
274 | $(node).addClass('reactive_record_is_loaded');
275 | }
276 | if (!$(while_loading_container).hasClass('reactive_record_is_loaded')) {
277 | var loading_children = $(while_loading_container).
278 | find('[data-reactive_record_enclosing_while_loading_container_id='+while_loading_container_id+'].reactive_record_is_loading')
279 | if (loading_children.length == 0) {
280 | $(while_loading_container).removeClass('reactive_record_is_loading')
281 | $(while_loading_container).addClass('reactive_record_is_loaded')
282 | }
283 | }
284 |
285 | }
286 |
287 | }
288 | }
289 |
290 | end
291 |
292 | end if RUBY_ENGINE == 'opal'
293 |
294 | end
295 |
--------------------------------------------------------------------------------
/spec/test_app/spec-opal/vendor/es5-shim.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * https://github.com/es-shims/es5-shim
3 | * @license es5-shim Copyright 2009-2014 by contributors, MIT License
4 | * see https://github.com/es-shims/es5-shim/blob/v4.1.0/LICENSE
5 | */
6 | (function(t,e){"use strict";if(typeof define==="function"&&define.amd){define(e)}else if(typeof exports==="object"){module.exports=e()}else{t.returnExports=e()}})(this,function(){var t=Array.prototype;var e=Object.prototype;var r=Function.prototype;var n=String.prototype;var i=Number.prototype;var a=t.slice;var o=t.splice;var u=t.push;var l=t.unshift;var f=r.call;var s=e.toString;var c=Array.isArray||function ye(t){return s.call(t)==="[object Array]"};var p=typeof Symbol==="function"&&typeof Symbol.toStringTag==="symbol";var h;var v=Function.prototype.toString,g=function de(t){try{v.call(t);return true}catch(e){return false}},y="[object Function]",d="[object GeneratorFunction]";h=function me(t){if(typeof t!=="function"){return false}if(p){return g(t)}var e=s.call(t);return e===y||e===d};var m;var b=RegExp.prototype.exec,w=function be(t){try{b.call(t);return true}catch(e){return false}},T="[object RegExp]";m=function we(t){if(typeof t!=="object"){return false}return p?w(t):s.call(t)===T};var x;var O=String.prototype.valueOf,j=function Te(t){try{O.call(t);return true}catch(e){return false}},S="[object String]";x=function xe(t){if(typeof t==="string"){return true}if(typeof t!=="object"){return false}return p?j(t):s.call(t)===S};var E=function Oe(t){var e=s.call(t);var r=e==="[object Arguments]";if(!r){r=!c(t)&&t!==null&&typeof t==="object"&&typeof t.length==="number"&&t.length>=0&&h(t.callee)}return r};var N=function(t){var e=Object.defineProperty&&function(){try{Object.defineProperty({},"x",{});return true}catch(t){return false}}();var r;if(e){r=function(t,e,r,n){if(!n&&e in t){return}Object.defineProperty(t,e,{configurable:true,enumerable:false,writable:true,value:r})}}else{r=function(t,e,r,n){if(!n&&e in t){return}t[e]=r}}return function n(e,i,a){for(var o in i){if(t.call(i,o)){r(e,o,i[o],a)}}}}(e.hasOwnProperty);function I(t){var e=typeof t;return t===null||e==="undefined"||e==="boolean"||e==="number"||e==="string"}var D={ToInteger:function je(t){var e=+t;if(e!==e){e=0}else if(e!==0&&e!==1/0&&e!==-(1/0)){e=(e>0||-1)*Math.floor(Math.abs(e))}return e},ToPrimitive:function Se(t){var e,r,n;if(I(t)){return t}r=t.valueOf;if(h(r)){e=r.call(t);if(I(e)){return e}}n=t.toString;if(h(n)){e=n.call(t);if(I(e)){return e}}throw new TypeError},ToObject:function(t){if(t==null){throw new TypeError("can't convert "+t+" to object")}return Object(t)},ToUint32:function Ee(t){return t>>>0}};var M=function Ne(){};N(r,{bind:function Ie(t){var e=this;if(!h(e)){throw new TypeError("Function.prototype.bind called on incompatible "+e)}var r=a.call(arguments,1);var n;var i=function(){if(this instanceof n){var i=e.apply(this,r.concat(a.call(arguments)));if(Object(i)===i){return i}return this}else{return e.apply(t,r.concat(a.call(arguments)))}};var o=Math.max(0,e.length-r.length);var u=[];for(var l=0;l0&&typeof e!=="number"){r=a.call(arguments);if(r.length<2){r.push(this.length-t)}else{r[1]=D.ToInteger(e)}}return o.apply(this,r)}},!U);var k=[].unshift(0)!==1;N(t,{unshift:function(){l.apply(this,arguments);return this.length}},k);N(Array,{isArray:c});var A=Object("a");var C=A[0]!=="a"||!(0 in A);var P=function Fe(t){var e=true;var r=true;if(t){t.call("foo",function(t,r,n){if(typeof n!=="object"){e=false}});t.call([1],function(){"use strict";r=typeof this==="string"},"x")}return!!t&&e&&r};N(t,{forEach:function Re(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=arguments[1],i=-1,a=r.length>>>0;if(!h(t)){throw new TypeError}while(++i>>0,i=Array(n),a=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var o=0;o>>0,i=[],a,o=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var u=0;u>>0,i=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var a=0;a>>0,i=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var a=0;a>>0;if(!h(t)){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduce of empty array with no initial value")}var i=0;var a;if(arguments.length>=2){a=arguments[1]}else{do{if(i in r){a=r[i++];break}if(++i>=n){throw new TypeError("reduce of empty array with no initial value")}}while(true)}for(;i>>0;if(!h(t)){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduceRight of empty array with no initial value")}var i,a=n-1;if(arguments.length>=2){i=arguments[1]}else{do{if(a in r){i=r[a--];break}if(--a<0){throw new TypeError("reduceRight of empty array with no initial value")}}while(true)}if(a<0){return i}do{if(a in r){i=t.call(void 0,i,r[a],a,e)}}while(a--);return i}},!J);var z=Array.prototype.indexOf&&[0,1].indexOf(1,2)!==-1;N(t,{indexOf:function Je(t){var e=C&&x(this)?this.split(""):D.ToObject(this),r=e.length>>>0;if(!r){return-1}var n=0;if(arguments.length>1){n=D.ToInteger(arguments[1])}n=n>=0?n:Math.max(0,r+n);for(;n>>0;if(!r){return-1}var n=r-1;if(arguments.length>1){n=Math.min(n,D.ToInteger(arguments[1]))}n=n>=0?n:r-Math.abs(n);for(;n>=0;n--){if(n in e&&t===e[n]){return n}}return-1}},$);var B=!{toString:null}.propertyIsEnumerable("toString"),G=function(){}.propertyIsEnumerable("prototype"),H=!F("x","0"),L=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],X=L.length;N(Object,{keys:function $e(t){var e=h(t),r=E(t),n=t!==null&&typeof t==="object",i=n&&x(t);if(!n&&!e&&!r){throw new TypeError("Object.keys called on a non-object")}var a=[];var o=G&&e;if(i&&H||r){for(var u=0;u9999?"+":"")+("00000"+Math.abs(n)).slice(0<=n&&n<=9999?-4:-6);e=t.length;while(e--){r=t[e];if(r<10){t[e]="0"+r}}return n+"-"+t.slice(0,2).join("-")+"T"+t.slice(2).join(":")+"."+("000"+this.getUTCMilliseconds()).slice(-3)+"Z"}},V);var W=false;try{W=Date.prototype.toJSON&&new Date(NaN).toJSON()===null&&new Date(K).toJSON().indexOf(Q)!==-1&&Date.prototype.toJSON.call({toISOString:function(){return true}})}catch(_){}if(!W){Date.prototype.toJSON=function He(t){var e=Object(this),r=D.ToPrimitive(e),n;if(typeof r==="number"&&!isFinite(r)){return null}n=e.toISOString;if(typeof n!=="function"){throw new TypeError("toISOString property is not callable")}return n.call(e)}}var te=Date.parse("+033658-09-27T01:46:40.000Z")===1e15;var ee=!isNaN(Date.parse("2012-04-04T24:00:00.500Z"))||!isNaN(Date.parse("2012-11-31T23:59:59.000Z"));var re=isNaN(Date.parse("2000-01-01T00:00:00.000Z"));if(!Date.parse||re||ee||!te){Date=function(t){function e(r,n,i,a,o,u,l){var f=arguments.length;if(this instanceof t){var s=f===1&&String(r)===r?new t(e.parse(r)):f>=7?new t(r,n,i,a,o,u,l):f>=6?new t(r,n,i,a,o,u):f>=5?new t(r,n,i,a,o):f>=4?new t(r,n,i,a):f>=3?new t(r,n,i):f>=2?new t(r,n):f>=1?new t(r):new t;s.constructor=e;return s}return t.apply(this,arguments)}var r=new RegExp("^"+"(\\d{4}|[+-]\\d{6})"+"(?:-(\\d{2})"+"(?:-(\\d{2})"+"(?:"+"T(\\d{2})"+":(\\d{2})"+"(?:"+":(\\d{2})"+"(?:(\\.\\d{1,}))?"+")?"+"("+"Z|"+"(?:"+"([-+])"+"(\\d{2})"+":(\\d{2})"+")"+")?)?)?)?"+"$");var n=[0,31,59,90,120,151,181,212,243,273,304,334,365];function i(t,e){var r=e>1?1:0;return n[e]+Math.floor((t-1969+r)/4)-Math.floor((t-1901+r)/100)+Math.floor((t-1601+r)/400)+365*(t-1970)}function a(e){return Number(new t(1970,0,1,0,0,0,e))}for(var o in t){e[o]=t[o]}e.now=t.now;e.UTC=t.UTC;e.prototype=t.prototype;e.prototype.constructor=e;e.parse=function u(e){var n=r.exec(e);if(n){var o=Number(n[1]),u=Number(n[2]||1)-1,l=Number(n[3]||1)-1,f=Number(n[4]||0),s=Number(n[5]||0),c=Number(n[6]||0),p=Math.floor(Number(n[7]||0)*1e3),h=Boolean(n[4]&&!n[8]),v=n[9]==="-"?1:-1,g=Number(n[10]||0),y=Number(n[11]||0),d;if(f<(s>0||c>0||p>0?24:25)&&s<60&&c<60&&p<1e3&&u>-1&&u<12&&g<24&&y<60&&l>-1&&l=0){r+=ie.data[e];ie.data[e]=Math.floor(r/t);r=r%t*ie.base}},numToString:function qe(){var t=ie.size;var e="";while(--t>=0){if(e!==""||t===0||ie.data[t]!==0){var r=String(ie.data[t]);if(e===""){e=r}else{e+="0000000".slice(0,7-r.length)+r}}}return e},pow:function Ke(t,e,r){return e===0?r:e%2===1?Ke(t,e-1,r*t):Ke(t*t,e/2,r)},log:function Qe(t){var e=0;while(t>=4096){e+=12;t/=4096}while(t>=2){e+=1;t/=2}return e}};N(i,{toFixed:function Ve(t){var e,r,n,i,a,o,u,l;e=Number(t);e=e!==e?0:Math.floor(e);if(e<0||e>20){throw new RangeError("Number.toFixed called with invalid number of decimals")}r=Number(this);if(r!==r){return"NaN"}if(r<=-1e21||r>=1e21){return String(r)}n="";if(r<0){n="-";r=-r}i="0";if(r>1e-21){a=ie.log(r*ie.pow(2,69,1))-69;o=a<0?r*ie.pow(2,-a,1):r/ie.pow(2,a,1);o*=4503599627370496;a=52-a;if(a>0){ie.multiply(0,o);u=e;while(u>=7){ie.multiply(1e7,0);u-=7}ie.multiply(ie.pow(10,u,1),0);u=a-1;while(u>=23){ie.divide(1<<23);u-=23}ie.divide(1<0){l=i.length;if(l<=e){i=n+"0.0000000000000000000".slice(0,e-l+2)+i}else{i=n+i.slice(0,l-e)+"."+i.slice(l-e)}}else{i=n+i}return i}},ne);var ae=n.split;if("ab".split(/(?:ab)*/).length!==2||".".split(/(.?)(.?)/).length!==4||"tesst".split(/(s)*/)[1]==="t"||"test".split(/(?:)/,-1).length!==4||"".split(/.?/).length||".".split(/()()/).length>1){(function(){var t=typeof/()??/.exec("")[1]==="undefined";n.split=function(e,r){var n=this;if(typeof e==="undefined"&&r===0){return[]}if(!m(e)){return ae.call(this,e,r)}var i=[],a=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.extended?"x":"")+(e.sticky?"y":""),o=0,l,f,s,c;e=new RegExp(e.source,a+"g");n+="";if(!t){l=new RegExp("^"+e.source+"$(?!\\s)",a)}r=typeof r==="undefined"?-1>>>0:D.ToUint32(r);f=e.exec(n);while(f){s=f.index+f[0].length;if(s>o){i.push(n.slice(o,f.index));if(!t&&f.length>1){f[0].replace(l,function(){for(var t=1;t1&&f.index=r){break}}if(e.lastIndex===f.index){e.lastIndex++}f=e.exec(n)}if(o===n.length){if(c||!e.test("")){i.push("")}}else{i.push(n.slice(o))}return i.length>r?i.slice(0,r):i}})()}else if("0".split(void 0,0).length){n.split=function We(t,e){if(typeof t==="undefined"&&e===0){return[]}return ae.call(this,t,e)}}var oe=n.replace;var ue=function(){var t=[];"x".replace(/x(.)?/g,function(e,r){t.push(r)});return t.length===1&&typeof t[0]==="undefined"}();if(!ue){n.replace=function _e(t,e){var r=h(e);var n=m(t)&&/\)[*?]/.test(t.source);if(!r||!n){return oe.call(this,t,e)}else{var i=function(r){var n=arguments.length;var i=t.lastIndex;t.lastIndex=0;var a=t.exec(r)||[];t.lastIndex=i;a.push(arguments[n-2],arguments[n-1]);return e.apply(this,a)};return oe.call(this,t,i)}}}var le=n.substr;var fe="".substr&&"0b".substr(-1)!=="b";N(n,{substr:function tr(t,e){return le.call(this,t<0?(t=this.length+t)<0?0:t:t,e)}},fe);var se=" \n\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003"+"\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028"+"\u2029\ufeff";var ce="\u200b";var pe="["+se+"]";var he=new RegExp("^"+pe+pe+"*");var ve=new RegExp(pe+pe+"*$");var ge=n.trim&&(se.trim()||!ce.trim());N(n,{trim:function er(){if(typeof this==="undefined"||this===null){throw new TypeError("can't convert "+this+" to object")}return String(this).replace(he,"").replace(ve,"")}},ge);if(parseInt(se+"08")!==8||parseInt(se+"0x16")!==22){parseInt=function(t){var e=/^0[xX]/;return function r(n,i){n=String(n).trim();if(!Number(i)){i=e.test(n)?16:10}return t(n,i)}}(parseInt)}});
7 |
--------------------------------------------------------------------------------