hyper-model 0.6.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +35 -41
- data/.rspec +2 -0
- data/.travis.yml +33 -0
- data/CHANGELOG.md +34 -0
- data/DOCS.md +735 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +298 -224
- data/{LICENSE → LICENSE.txt} +6 -6
- data/README.md +51 -2
- data/Rakefile +18 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/codeship.database.yml +18 -0
- data/hyper-model.gemspec +62 -36
- data/lib/active_model_client_stubs.rb +16 -0
- data/lib/active_record_base.rb +331 -0
- data/{examples/chat-app/app/assets/images/.keep → lib/acts_as_string.rb} +0 -0
- data/lib/enumerable/pluck.rb +6 -0
- data/lib/hyper-model.rb +59 -8
- data/lib/hyper_model/version.rb +3 -0
- data/lib/hyper_react/input_tags.rb +47 -0
- data/lib/hyperloop/model/load.rb +1 -1
- data/lib/kernel/itself.rb +5 -0
- data/lib/object/tap.rb +7 -0
- data/lib/opal/equality_patches.rb +15 -0
- data/lib/opal/parse_patch.rb +14 -0
- data/lib/opal/set_patches.rb +8 -0
- data/lib/reactive_record/active_record/aggregations.rb +69 -0
- data/lib/reactive_record/active_record/associations.rb +118 -0
- data/lib/reactive_record/active_record/base.rb +10 -0
- data/lib/reactive_record/active_record/class_methods.rb +406 -0
- data/lib/reactive_record/active_record/error.rb +31 -0
- data/lib/reactive_record/active_record/errors.rb +374 -0
- data/lib/reactive_record/active_record/instance_methods.rb +187 -0
- data/lib/reactive_record/active_record/public_columns_hash.rb +44 -0
- data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +36 -0
- data/lib/reactive_record/active_record/reactive_record/base.rb +416 -0
- data/lib/reactive_record/active_record/reactive_record/collection.rb +558 -0
- data/lib/reactive_record/active_record/reactive_record/column_types.rb +75 -0
- data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +236 -0
- data/lib/reactive_record/active_record/reactive_record/getters.rb +133 -0
- data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +576 -0
- data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +54 -0
- data/lib/reactive_record/active_record/reactive_record/operations.rb +107 -0
- data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +62 -0
- data/lib/reactive_record/active_record/reactive_record/setters.rb +194 -0
- data/lib/reactive_record/active_record/reactive_record/unscoped_collection.rb +16 -0
- data/lib/reactive_record/active_record/reactive_record/while_loading.rb +343 -0
- data/lib/reactive_record/active_record_error.rb +4 -0
- data/lib/reactive_record/broadcast.rb +223 -0
- data/lib/reactive_record/engine.rb +11 -0
- data/lib/reactive_record/interval.rb +190 -0
- data/lib/reactive_record/permissions.rb +117 -0
- data/lib/reactive_record/pry.rb +13 -0
- data/lib/reactive_record/reactive_scope.rb +18 -0
- data/lib/reactive_record/scope_description.rb +121 -0
- data/lib/reactive_record/serializers.rb +7 -0
- data/lib/reactive_record/server_data_cache.rb +478 -0
- data/path_release_steps.md +9 -0
- metadata +399 -109
- data/CODE_OF_CONDUCT.md +0 -49
- data/examples/chat-app/.gitignore +0 -21
- data/examples/chat-app/Gemfile +0 -62
- data/examples/chat-app/Gemfile.lock +0 -309
- data/examples/chat-app/README.md +0 -3
- data/examples/chat-app/Rakefile +0 -6
- data/examples/chat-app/app/assets/config/manifest.js +0 -3
- data/examples/chat-app/app/assets/javascripts/application.js +0 -3
- data/examples/chat-app/app/assets/stylesheets/application.scss +0 -33
- data/examples/chat-app/app/controllers/application_controller.rb +0 -3
- data/examples/chat-app/app/controllers/home_controller.rb +0 -5
- data/examples/chat-app/app/hyperloop/components/app.rb +0 -12
- data/examples/chat-app/app/hyperloop/components/formatted_div.rb +0 -15
- data/examples/chat-app/app/hyperloop/components/input_box.rb +0 -26
- data/examples/chat-app/app/hyperloop/components/message.rb +0 -29
- data/examples/chat-app/app/hyperloop/components/messages.rb +0 -8
- data/examples/chat-app/app/hyperloop/components/nav.rb +0 -30
- data/examples/chat-app/app/hyperloop/models/application_record.rb +0 -3
- data/examples/chat-app/app/hyperloop/models/message.rb +0 -6
- data/examples/chat-app/app/hyperloop/operations/operations.rb +0 -13
- data/examples/chat-app/app/hyperloop/stores/message_store.rb +0 -17
- data/examples/chat-app/app/policies/application_policy.rb +0 -9
- data/examples/chat-app/app/views/layouts/application.html.erb +0 -12
- data/examples/chat-app/bin/bundle +0 -3
- data/examples/chat-app/bin/rails +0 -9
- data/examples/chat-app/bin/rake +0 -9
- data/examples/chat-app/bin/setup +0 -34
- data/examples/chat-app/bin/spring +0 -17
- data/examples/chat-app/bin/update +0 -29
- data/examples/chat-app/config.ru +0 -5
- data/examples/chat-app/config/application.rb +0 -12
- data/examples/chat-app/config/boot.rb +0 -3
- data/examples/chat-app/config/cable.yml +0 -9
- data/examples/chat-app/config/database.yml +0 -25
- data/examples/chat-app/config/environment.rb +0 -5
- data/examples/chat-app/config/environments/development.rb +0 -56
- data/examples/chat-app/config/environments/production.rb +0 -86
- data/examples/chat-app/config/environments/test.rb +0 -42
- data/examples/chat-app/config/initializers/application_controller_renderer.rb +0 -6
- data/examples/chat-app/config/initializers/assets.rb +0 -11
- data/examples/chat-app/config/initializers/backtrace_silencers.rb +0 -7
- data/examples/chat-app/config/initializers/cookies_serializer.rb +0 -5
- data/examples/chat-app/config/initializers/filter_parameter_logging.rb +0 -4
- data/examples/chat-app/config/initializers/hyperloop.rb +0 -6
- data/examples/chat-app/config/initializers/inflections.rb +0 -16
- data/examples/chat-app/config/initializers/mime_types.rb +0 -4
- data/examples/chat-app/config/initializers/new_framework_defaults.rb +0 -24
- data/examples/chat-app/config/initializers/session_store.rb +0 -3
- data/examples/chat-app/config/initializers/wrap_parameters.rb +0 -14
- data/examples/chat-app/config/locales/en.yml +0 -23
- data/examples/chat-app/config/puma.rb +0 -47
- data/examples/chat-app/config/routes.rb +0 -5
- data/examples/chat-app/config/secrets.yml +0 -22
- data/examples/chat-app/config/spring.rb +0 -6
- data/examples/chat-app/db/migrate/20170319194429_create_message.rb +0 -9
- data/examples/chat-app/db/schema.rb +0 -48
- data/examples/chat-app/db/seeds.rb +0 -7
- data/examples/chat-app/lib/assets/.keep +0 -0
- data/examples/chat-app/lib/tasks/.keep +0 -0
- data/examples/chat-app/log/.keep +0 -0
- data/examples/chat-app/public/404.html +0 -67
- data/examples/chat-app/public/422.html +0 -67
- data/examples/chat-app/public/500.html +0 -66
- data/examples/chat-app/public/apple-touch-icon-precomposed.png +0 -0
- data/examples/chat-app/public/apple-touch-icon.png +0 -0
- data/examples/chat-app/public/favicon.ico +0 -0
- data/examples/chat-app/public/robots.txt +0 -5
- data/examples/chat-app/test/controllers/.keep +0 -0
- data/examples/chat-app/test/fixtures/.keep +0 -0
- data/examples/chat-app/test/fixtures/files/.keep +0 -0
- data/examples/chat-app/test/helpers/.keep +0 -0
- data/examples/chat-app/test/integration/.keep +0 -0
- data/examples/chat-app/test/mailers/.keep +0 -0
- data/examples/chat-app/test/models/.keep +0 -0
- data/examples/chat-app/test/test_helper.rb +0 -10
- data/examples/chat-app/tmp/.keep +0 -0
- data/examples/chat-app/vendor/assets/javascripts/.keep +0 -0
- data/examples/chat-app/vendor/assets/stylesheets/.keep +0 -0
- data/lib/hyperloop/model/version.rb +0 -5
data/{LICENSE → LICENSE.txt}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
MIT License
|
|
1
|
+
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2016 Mitch VanDuyn
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
9
9
|
copies of the Software, and to permit persons to whom the Software is
|
|
10
10
|
furnished to do so, subject to the following conditions:
|
|
11
11
|
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
copies or substantial portions of the Software.
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
14
|
|
|
15
15
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
16
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
17
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
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
|
|
21
|
-
SOFTWARE.
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,2 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
<div class="githubhyperloopheader">
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
|
|
5
|
+
<a href="http://ruby-hyperloop.org/" alt="Hyperloop" title="Hyperloop">
|
|
6
|
+
<img width="350px" src="http://ruby-hyperloop.org/images/hyperloop-github-logo.png">
|
|
7
|
+
</a>
|
|
8
|
+
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<h2 align="center">The Complete Isomorphic Ruby Framework</h2>
|
|
12
|
+
|
|
13
|
+
<br>
|
|
14
|
+
|
|
15
|
+
<a href="http://ruby-hyperloop.org/" alt="Hyperloop" title="Hyperloop">
|
|
16
|
+
<img src="http://ruby-hyperloop.org/images/githubhyperloopbadge.png">
|
|
17
|
+
</a>
|
|
18
|
+
|
|
19
|
+
<a href="https://gitter.im/ruby-hyperloop/chat" alt="Gitter chat" title="Gitter chat">
|
|
20
|
+
<img src="http://ruby-hyperloop.org/images/githubgitterbadge.png">
|
|
21
|
+
</a>
|
|
22
|
+
|
|
23
|
+
[](https://travis-ci.org/ruby-hyperloop/hyper-mesh)
|
|
24
|
+
[](https://app.codeship.com/projects/202301)
|
|
25
|
+
[](https://badge.fury.io/rb/hyper-mesh)
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="http://ruby-hyperloop.org/images/HyperModels.png" width="100" alt="Hyper-models">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
## Hyper-Mesh GEM is part of Hyperloop GEMS family
|
|
34
|
+
|
|
35
|
+
Hyper-Mesh GEM comes with the Hyperloop GEM.
|
|
36
|
+
|
|
37
|
+
But if you want to install it separately, please install the [Hyper-model GEM](https://github.com/ruby-hyperloop/hyper-model).
|
|
38
|
+
|
|
39
|
+
## Community
|
|
40
|
+
|
|
41
|
+
#### Getting Help
|
|
42
|
+
Please **do not post** usage questions to GitHub Issues. For these types of questions use our [Gitter chatroom](https://gitter.im/ruby-hyperloop/chat) or [StackOverflow](http://stackoverflow.com/questions/tagged/hyperloop).
|
|
43
|
+
|
|
44
|
+
#### Submitting Bugs and Enhancements
|
|
45
|
+
[GitHub Issues](https://github.com/ruby-hyperloop/hyperloop/issues) is for suggesting enhancements and reporting bugs. Before submiting a bug make sure you do the following:
|
|
46
|
+
* Check out our [contributing guide](https://github.com/ruby-hyperloop/hyperloop/blob/master/CONTRIBUTING.md) for info on our release cycle.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
Hyperloop is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
|
51
|
+
|
data/Rakefile
CHANGED
|
@@ -1,2 +1,20 @@
|
|
|
1
1
|
require "bundler/gem_tasks"
|
|
2
|
+
require "rspec/core/rake_task"
|
|
3
|
+
|
|
4
|
+
task :spec do
|
|
5
|
+
(1..7).each { |batch| Rake::Task["spec:batch#{batch}"].invoke rescue nil }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
namespace :spec do
|
|
9
|
+
task :prepare do
|
|
10
|
+
sh %(cd spec/test_app; bundle exec rails db:setup)
|
|
11
|
+
end
|
|
12
|
+
(1..7).each do |batch|
|
|
13
|
+
RSpec::Core::RakeTask.new(:"batch#{batch}") do |t|
|
|
14
|
+
t.fail_on_error = false unless batch == 7
|
|
15
|
+
t.pattern = "spec/batch#{batch}/**/*_spec.rb"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
2
20
|
task :default => :spec
|
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "hyper-mesh"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
development:
|
|
2
|
+
adapter: mysql2
|
|
3
|
+
host: localhost
|
|
4
|
+
encoding: utf8
|
|
5
|
+
pool: 10
|
|
6
|
+
username: <%= ENV['MYSQL_USER'] %>
|
|
7
|
+
password: <%= ENV['MYSQL_PASSWORD'] %>
|
|
8
|
+
database: development<%= ENV['TEST_ENV_NUMBER'] %>
|
|
9
|
+
socket: /var/run/mysqld/mysqld.sock
|
|
10
|
+
test:
|
|
11
|
+
adapter: mysql2
|
|
12
|
+
host: localhost
|
|
13
|
+
encoding: utf8
|
|
14
|
+
pool: 10
|
|
15
|
+
username: <%= ENV['MYSQL_USER'] %>
|
|
16
|
+
password: <%= ENV['MYSQL_PASSWORD'] %>
|
|
17
|
+
database: test<%= ENV['TEST_ENV_NUMBER'] %>
|
|
18
|
+
socket: /var/run/mysqld/mysqld.sock
|
data/hyper-model.gemspec
CHANGED
|
@@ -1,43 +1,69 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
require 'hyperloop/model/version'
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path('../lib/', __FILE__)
|
|
3
|
+
require 'hyper_model/version'
|
|
5
4
|
|
|
6
5
|
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name =
|
|
8
|
-
spec.version =
|
|
9
|
-
spec.authors = [
|
|
10
|
-
spec.email = [
|
|
6
|
+
spec.name = 'hyper-model'
|
|
7
|
+
spec.version = HyperModel::VERSION
|
|
8
|
+
spec.authors = ['Mitch VanDuyn', 'Jan Biedermann']
|
|
9
|
+
spec.email = ['mitch@catprint.com', 'jan@kursator.com']
|
|
10
|
+
spec.summary = 'React based CRUD access and Synchronization of active record models across multiple clients'
|
|
11
|
+
spec.description = 'HyperModel gives your HyperComponents CRUD access to your '\
|
|
12
|
+
'ActiveRecord models on the client, using the the standard ActiveRecord '\
|
|
13
|
+
'API. HyperModel also implements push notifications (via a number of '\
|
|
14
|
+
'possible technologies) so changes to records on the server are '\
|
|
15
|
+
'dynamically updated on all authorised clients.'
|
|
16
|
+
spec.homepage = 'http://ruby-hyperloop.org'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
# spec.metadata = {
|
|
19
|
+
# "homepage_uri" => 'http://ruby-hyperloop.org',
|
|
20
|
+
# "source_code_uri" => 'https://github.com/ruby-hyperloop/hyper-component'
|
|
21
|
+
# }
|
|
11
22
|
|
|
12
|
-
spec.
|
|
13
|
-
spec.
|
|
14
|
-
spec.
|
|
23
|
+
spec.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^(examples|gemfiles|pkg|reactive_record_test_app|spec)/}) }
|
|
24
|
+
# spec.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
25
|
+
spec.test_files = `git ls-files -- {spec}/*`.split("\n")
|
|
26
|
+
spec.require_paths = ['lib']
|
|
15
27
|
|
|
16
|
-
spec.
|
|
17
|
-
spec.
|
|
18
|
-
spec.
|
|
19
|
-
spec.
|
|
20
|
-
|
|
21
|
-
spec.
|
|
22
|
-
|
|
23
|
-
spec.add_development_dependency '
|
|
28
|
+
spec.add_dependency 'activemodel'
|
|
29
|
+
spec.add_dependency 'activerecord', '>= 4.0.0'
|
|
30
|
+
spec.add_dependency 'hyper-component', HyperModel::VERSION
|
|
31
|
+
spec.add_dependency 'hyper-operation', HyperModel::VERSION
|
|
32
|
+
spec.add_development_dependency 'bundler'
|
|
33
|
+
spec.add_development_dependency 'capybara'
|
|
34
|
+
spec.add_development_dependency 'chromedriver-helper', '1.2.0'
|
|
35
|
+
spec.add_development_dependency 'libv8', '~> 6.3.0' # see https://github.com/discourse/mini_racer/issues/92
|
|
36
|
+
spec.add_development_dependency 'mini_racer', '~> 0.1.15'
|
|
37
|
+
spec.add_development_dependency 'selenium-webdriver'
|
|
24
38
|
spec.add_development_dependency 'database_cleaner'
|
|
25
|
-
spec.add_development_dependency '
|
|
26
|
-
spec.add_development_dependency '
|
|
27
|
-
spec.add_development_dependency '
|
|
28
|
-
spec.add_development_dependency 'opal-
|
|
29
|
-
spec.add_development_dependency '
|
|
30
|
-
spec.add_development_dependency 'rails'
|
|
31
|
-
spec.add_development_dependency '
|
|
32
|
-
spec.add_development_dependency '
|
|
33
|
-
spec.add_development_dependency '
|
|
34
|
-
spec.add_development_dependency 'factory_girl_rails'
|
|
35
|
-
spec.add_development_dependency 'sqlite3'
|
|
39
|
+
spec.add_development_dependency 'factory_bot_rails'
|
|
40
|
+
#spec.add_development_dependency 'hyper-spec', HyperModel::VERSION
|
|
41
|
+
spec.add_development_dependency 'mysql2'
|
|
42
|
+
spec.add_development_dependency 'opal-activesupport', '~> 0.3.1'
|
|
43
|
+
spec.add_development_dependency 'opal-browser', '~> 0.2.0'
|
|
44
|
+
spec.add_development_dependency 'opal-rails', '~> 0.9.4'
|
|
45
|
+
spec.add_development_dependency 'parser'
|
|
46
|
+
spec.add_development_dependency 'pry'
|
|
47
|
+
spec.add_development_dependency 'pry-rescue'
|
|
36
48
|
spec.add_development_dependency 'puma'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
spec.add_development_dependency '
|
|
40
|
-
|
|
41
|
-
spec.add_development_dependency '
|
|
42
|
-
spec.add_development_dependency '
|
|
49
|
+
spec.add_development_dependency 'pusher'
|
|
50
|
+
spec.add_development_dependency 'pusher-fake'
|
|
51
|
+
spec.add_development_dependency 'rails', '>= 4.0.0'
|
|
52
|
+
spec.add_development_dependency 'rake'
|
|
53
|
+
spec.add_development_dependency 'react-rails', '>= 2.4.0', '< 2.5.0'
|
|
54
|
+
spec.add_development_dependency 'reactrb-rails-generator'
|
|
55
|
+
spec.add_development_dependency 'rspec-collection_matchers'
|
|
56
|
+
spec.add_development_dependency 'rspec-expectations'
|
|
57
|
+
spec.add_development_dependency 'rspec-its'
|
|
58
|
+
spec.add_development_dependency 'rspec-mocks'
|
|
59
|
+
spec.add_development_dependency 'rspec-rails'
|
|
60
|
+
spec.add_development_dependency 'rspec-steps', '~> 2.1.1'
|
|
61
|
+
spec.add_development_dependency 'rspec-wait'
|
|
62
|
+
spec.add_development_dependency 'rubocop', '~> 0.51.0'
|
|
63
|
+
spec.add_development_dependency 'shoulda'
|
|
64
|
+
spec.add_development_dependency 'shoulda-matchers'
|
|
65
|
+
spec.add_development_dependency 'spring-commands-rspec'
|
|
66
|
+
spec.add_development_dependency 'sqlite3'
|
|
67
|
+
spec.add_development_dependency 'timecop', '~> 0.8.1'
|
|
68
|
+
spec.add_development_dependency 'unparser'
|
|
43
69
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ActiveModel
|
|
2
|
+
# minimal ActiveModel definitions so we can support I18n methods
|
|
3
|
+
class Name
|
|
4
|
+
attr_reader :name, :klass, :i18n_key
|
|
5
|
+
|
|
6
|
+
def initialize(klass)
|
|
7
|
+
@name = klass.name
|
|
8
|
+
@klass = klass
|
|
9
|
+
@i18n_key = :"#{@name.underscore}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
@name
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Monkey patches to ActiveRecord for scoping, security, and to synchronize models
|
|
2
|
+
module ActiveRecord
|
|
3
|
+
# hyperloop adds new features to scopes to allow for computing scopes on client side
|
|
4
|
+
# and for hinting at what joins are involved in a scope. _synchromesh_scope_args_check
|
|
5
|
+
# processes these arguments, and the will always leave the true server side scoping
|
|
6
|
+
# proc in the `:server` opts. This method is common to client and server.
|
|
7
|
+
class Base
|
|
8
|
+
def self._synchromesh_scope_args_check(args)
|
|
9
|
+
opts = if args.count == 2 && args[1].is_a?(Hash)
|
|
10
|
+
args[1].merge(server: args[0])
|
|
11
|
+
elsif args[0].is_a? Hash
|
|
12
|
+
args[0]
|
|
13
|
+
else
|
|
14
|
+
{ server: args[0] }
|
|
15
|
+
end
|
|
16
|
+
return opts if opts && opts[:server].respond_to?(:call)
|
|
17
|
+
raise 'must provide either a proc as the first arg or by the '\
|
|
18
|
+
'`:server` option to scope and default_scope methods'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
if RUBY_ENGINE != 'opal'
|
|
22
|
+
# __synchromesh_permission_granted indicates if permission has been given to return a scope
|
|
23
|
+
# The acting_user attribute is set to the current acting_user so regulation methods can check it
|
|
24
|
+
# The __secure_collection_check method is called at the end of a scope chain and will fail if
|
|
25
|
+
# no scope in the chain has positively granted access.
|
|
26
|
+
|
|
27
|
+
# allows us to easily handle scopes and finder_methods which return arrays of items
|
|
28
|
+
# (instead of ActiveRecord::Relations - see below)
|
|
29
|
+
class ReactiveRecordPsuedoRelationArray < Array
|
|
30
|
+
attr_accessor :__synchromesh_permission_granted
|
|
31
|
+
attr_accessor :acting_user
|
|
32
|
+
def __secure_collection_check(_acting_user)
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# add the __synchromesh_permission_granted, acting_user and __secure_collection_check
|
|
38
|
+
# methods to Relation
|
|
39
|
+
class Relation
|
|
40
|
+
attr_accessor :__synchromesh_permission_granted
|
|
41
|
+
attr_accessor :acting_user
|
|
42
|
+
def __secure_collection_check(acting_user)
|
|
43
|
+
return self if __synchromesh_permission_granted
|
|
44
|
+
return self if __secure_remote_access_to_all(self, acting_user).__synchromesh_permission_granted
|
|
45
|
+
return self if __secure_remote_access_to_unscoped(self, acting_user).__synchromesh_permission_granted
|
|
46
|
+
Hyperloop::InternalPolicy.raise_operation_access_violation(:scoped_permission_not_granted, "Last relation: #{self}, acting_user: #{acting_user}")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
# Monkey patches and extensions to base
|
|
50
|
+
class Base
|
|
51
|
+
class << self
|
|
52
|
+
# every method call that is legal from the client has a wrapper method prefixed with
|
|
53
|
+
# __secure_remote_access_to_
|
|
54
|
+
|
|
55
|
+
# The wrapper method may simply return the normal result or may act to secure the data.
|
|
56
|
+
# The simpliest case is for the method to call `denied!` which will raise a Hyperloop
|
|
57
|
+
# access protection fault.
|
|
58
|
+
|
|
59
|
+
def denied!
|
|
60
|
+
Hyperloop::InternalPolicy.raise_operation_access_violation(:scoped_denied, "#{self} regulation denies scope access. Called from #{caller_locations(1)}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Here we set up the base `all` and `unscoped` methods. See below for more on how
|
|
64
|
+
# access protection works on relationships.
|
|
65
|
+
|
|
66
|
+
def __secure_remote_access_to_all(_self, _acting_user)
|
|
67
|
+
all
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def __secure_remote_access_to_unscoped(_self, _acting_user, *args)
|
|
71
|
+
unscoped(*args)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# finder_method and server_method provide secure RPCs against AR relations and records.
|
|
75
|
+
# The block is called in context with the object, and acting_user is set to the
|
|
76
|
+
# current acting user. The block may interogate acting_user to insure security as needed.
|
|
77
|
+
|
|
78
|
+
# For finder_method we have to preapply `all` so that we always have a relationship
|
|
79
|
+
|
|
80
|
+
def finder_method(name, &block)
|
|
81
|
+
singleton_class.send(:define_method, :"__secure_remote_access_to__#{name}") do |this, acting_user, *args|
|
|
82
|
+
this = respond_to?(:acting_user) ? this : all
|
|
83
|
+
begin
|
|
84
|
+
old = this.acting_user
|
|
85
|
+
this.acting_user = acting_user
|
|
86
|
+
# returns a PsuedoRelationArray which will respond to the
|
|
87
|
+
# __secure_collection_check method
|
|
88
|
+
ReactiveRecordPsuedoRelationArray.new([this.instance_exec(*args, &block)])
|
|
89
|
+
ensure
|
|
90
|
+
this.acting_user = old
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
singleton_class.send(:define_method, name) do |*args|
|
|
94
|
+
all.instance_exec(*args, &block)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def server_method(name, _opts = {}, &block)
|
|
99
|
+
# callable from the server internally
|
|
100
|
+
define_method(name, &block)
|
|
101
|
+
# callable remotely from the client
|
|
102
|
+
define_method("__secure_remote_access_to_#{name}") do |_self, acting_user, *args|
|
|
103
|
+
begin
|
|
104
|
+
old = self.acting_user
|
|
105
|
+
self.acting_user = acting_user
|
|
106
|
+
send(name, *args)
|
|
107
|
+
ensure
|
|
108
|
+
self.acting_user = old
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# relationships (and scopes) are regulated using a tri-state system. Each
|
|
114
|
+
# remote access method will return the relationship as normal but will also set
|
|
115
|
+
# the value of __secure_remote_access_granted using the application defined regulation.
|
|
116
|
+
# Each regulation can explicitly allow the scope to be chained by returning a truthy
|
|
117
|
+
# value from the regulation. Or each regulation can explicitly deny the scope to
|
|
118
|
+
# be chained by called `denied!`. Otherwise each regulation can return a falsy
|
|
119
|
+
# value meaning the scope can be changed, but unless some other scope (before or
|
|
120
|
+
# after) in the chain explicitly allows the scope, the entire chain will fail.
|
|
121
|
+
|
|
122
|
+
# In otherwords within a chain of relationships and scopes, at least one Regulation
|
|
123
|
+
# must be return a truthy value otherwise the whole chain fails. Likewise if any
|
|
124
|
+
# regulation called `deined!` the whole chain fails.
|
|
125
|
+
|
|
126
|
+
# If no regulation is defined, the regulation is inherited from the superclass, and if
|
|
127
|
+
# no regulation is defined anywhere in the class heirarchy then the regulation will
|
|
128
|
+
# return a falsy value.
|
|
129
|
+
|
|
130
|
+
# regulations on scopes are inheritable. That is if a superclass defines a regulation
|
|
131
|
+
# for a scope, subclasses will inherit the regulation (but can override)
|
|
132
|
+
|
|
133
|
+
# helper method to sort out the options on the regulate_scope, regulate_relationship macros.
|
|
134
|
+
|
|
135
|
+
# We allow three forms:
|
|
136
|
+
# regulate_xxx name &block : the block is the regulation
|
|
137
|
+
# regulate_xxx name: const : const can be denied!, deny, denied, or any other truthy or
|
|
138
|
+
# falsy value
|
|
139
|
+
# regulate_xxx name: proc : the proc is the regulation
|
|
140
|
+
|
|
141
|
+
def __synchromesh_parse_regulator_params(name, block)
|
|
142
|
+
if name.is_a? Hash
|
|
143
|
+
name, block = name.first
|
|
144
|
+
if %i[denied! deny denied].include? block
|
|
145
|
+
block = ->(*_args) { denied! }
|
|
146
|
+
elsif !block.is_a? Proc
|
|
147
|
+
value = block
|
|
148
|
+
block = ->(*_args) { value }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
[name, block || ->(*_args) { true }]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# helper method for providing a regulation in line with a scope or relationship
|
|
155
|
+
# this is done using the `regulate` key on the opts.
|
|
156
|
+
# if no regulate key is provided and there is no regulation already defined for
|
|
157
|
+
# this name, then we create one that returns nil (don't care)
|
|
158
|
+
# once we have things figured out, we yield to the provided proc which is either
|
|
159
|
+
# regulate_scope or regulate_relationship
|
|
160
|
+
|
|
161
|
+
def __synchromesh_regulate_from_macro(opts, name, already_defined)
|
|
162
|
+
if opts.key?(:regulate)
|
|
163
|
+
yield name => opts[:regulate]
|
|
164
|
+
elsif !already_defined
|
|
165
|
+
yield name => ->(*_args) {}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# helper method to set the value of __synchromesh_permission_granted on the relationship
|
|
170
|
+
# Set acting_user on the object, then or in the result of running the block in context
|
|
171
|
+
# of the obj with the current value of __synchromesh_permission_granted
|
|
172
|
+
|
|
173
|
+
def __set_synchromesh_permission_granted(old_rel, new_rel, obj, acting_user, args = [], &block)
|
|
174
|
+
saved_acting_user = obj.acting_user
|
|
175
|
+
obj.acting_user = acting_user
|
|
176
|
+
new_rel.__synchromesh_permission_granted =
|
|
177
|
+
obj.instance_exec(*args, &block) || (old_rel && old_rel.try(:__synchromesh_permission_granted))
|
|
178
|
+
new_rel
|
|
179
|
+
ensure
|
|
180
|
+
obj.acting_user = saved_acting_user
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# regulate scope has to deal with the special case that the scope returns an
|
|
184
|
+
# an array instead of a relationship. In this case we wrap the array and go on
|
|
185
|
+
|
|
186
|
+
def regulate_scope(name, &block)
|
|
187
|
+
name, block = __synchromesh_parse_regulator_params(name, block)
|
|
188
|
+
singleton_class.send(:define_method, :"__secure_remote_access_to_#{name}") do |this, acting_user, *args|
|
|
189
|
+
r = this.send(name, *args)
|
|
190
|
+
r = ReactiveRecordPsuedoRelationArray.new(r) if r.is_a? Array
|
|
191
|
+
__set_synchromesh_permission_granted(this, r, r, acting_user, args, &block)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# regulate_default_scope
|
|
196
|
+
|
|
197
|
+
def regulate_default_scope(*args, &block)
|
|
198
|
+
block = __synchromesh_parse_regulator_params({ all: args[0] }, block).last unless args.empty?
|
|
199
|
+
regulate_scope(:all, &block)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# monkey patch scope and default_scope macros to process hyperloop special opts,
|
|
203
|
+
# and add regulations if present
|
|
204
|
+
|
|
205
|
+
alias pre_synchromesh_scope scope
|
|
206
|
+
|
|
207
|
+
def scope(name, *args, &block)
|
|
208
|
+
__synchromesh_regulate_from_macro(
|
|
209
|
+
(opts = _synchromesh_scope_args_check(args)),
|
|
210
|
+
name,
|
|
211
|
+
respond_to?(:"__secure_remote_access_to_#{name}"),
|
|
212
|
+
&method(:regulate_scope)
|
|
213
|
+
)
|
|
214
|
+
pre_synchromesh_scope(name, opts[:server], &block)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
alias pre_synchromesh_default_scope default_scope
|
|
218
|
+
|
|
219
|
+
def default_scope(*args, &block)
|
|
220
|
+
__synchromesh_regulate_from_macro(
|
|
221
|
+
(opts = _synchromesh_scope_args_check([*block, *args])),
|
|
222
|
+
:all,
|
|
223
|
+
respond_to?(:__secure_remote_access_to_all),
|
|
224
|
+
&method(:regulate_scope)
|
|
225
|
+
)
|
|
226
|
+
pre_synchromesh_default_scope(opts[:server], &block)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# add regulate_relationship method and monkey patch has_many macro
|
|
230
|
+
# to add regulations if present
|
|
231
|
+
|
|
232
|
+
def regulate_relationship(name, &block)
|
|
233
|
+
name, block = __synchromesh_parse_regulator_params(name, block)
|
|
234
|
+
define_method(:"__secure_remote_access_to_#{name}") do |this, acting_user, *args|
|
|
235
|
+
this.class.__set_synchromesh_permission_granted(
|
|
236
|
+
nil, this.send(name, *args), this, acting_user, &block
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
alias pre_syncromesh_has_many has_many
|
|
242
|
+
|
|
243
|
+
def has_many(name, *args, &block)
|
|
244
|
+
__synchromesh_regulate_from_macro(
|
|
245
|
+
opts = args.extract_options!,
|
|
246
|
+
name,
|
|
247
|
+
method_defined?(:"__secure_remote_access_to_#{name}"),
|
|
248
|
+
&method(:regulate_relationship)
|
|
249
|
+
)
|
|
250
|
+
pre_syncromesh_has_many name, *args, opts.except(:regulate), &block
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# add secure access for find, find_by, and belongs_to and has_one relations.
|
|
254
|
+
# No explicit security checks are needed here, as the data returned by these objects
|
|
255
|
+
# will be further processedand checked before returning. I.e. it is not possible to
|
|
256
|
+
# simply return `find(1)` but if you try returning `find(1).name` the permission system
|
|
257
|
+
# will check to see if the name attribute can be legally sent to the current acting user.
|
|
258
|
+
|
|
259
|
+
def __secure_remote_access_to_find(_self, _acting_user, *args)
|
|
260
|
+
find(*args)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def __secure_remote_access_to_find_by(_self, _acting_user, *args)
|
|
264
|
+
find_by(*args)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
%i[belongs_to has_one].each do |macro|
|
|
268
|
+
alias_method :"pre_syncromesh_#{macro}", macro
|
|
269
|
+
define_method(macro) do |name, *aargs, &block|
|
|
270
|
+
define_method(:"__secure_remote_access_to_#{name}") do |this, _acting_user, *args|
|
|
271
|
+
this.send(name, *args)
|
|
272
|
+
end
|
|
273
|
+
send(:"pre_syncromesh_#{macro}", name, *aargs, &block)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def denied!
|
|
279
|
+
Hyperloop::InternalPolicy.raise_operation_access_violation(:scoped_denied, "#{self.class} regulation denies scope access. Called from #{caller_locations(1)}")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# call do_not_synchronize to block synchronization of a model
|
|
283
|
+
|
|
284
|
+
def self.do_not_synchronize
|
|
285
|
+
@do_not_synchronize = true
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# used by the broadcast mechanism to determine if this model is to be synchronized
|
|
289
|
+
|
|
290
|
+
def self.do_not_synchronize?
|
|
291
|
+
@do_not_synchronize
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def do_not_synchronize?
|
|
295
|
+
self.class.do_not_synchronize?
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
after_commit :synchromesh_after_create, on: [:create]
|
|
299
|
+
after_commit :synchromesh_after_change, on: [:update]
|
|
300
|
+
after_commit :synchromesh_after_destroy, on: [:destroy]
|
|
301
|
+
|
|
302
|
+
def synchromesh_after_create
|
|
303
|
+
return if do_not_synchronize?
|
|
304
|
+
ReactiveRecord::Broadcast.after_commit :create, self
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def synchromesh_after_change
|
|
308
|
+
return if do_not_synchronize? || previous_changes.empty?
|
|
309
|
+
ReactiveRecord::Broadcast.after_commit :change, self
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def synchromesh_after_destroy
|
|
313
|
+
return if do_not_synchronize?
|
|
314
|
+
ReactiveRecord::Broadcast.after_commit :destroy, self
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def __hyperloop_secure_attributes(acting_user)
|
|
318
|
+
accessible_attributes =
|
|
319
|
+
Hyperloop::InternalPolicy.accessible_attributes_for(self, acting_user)
|
|
320
|
+
attributes.select { |attr| accessible_attributes.include? attr.to_sym }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# regulate built in scopes so they are accesible from the client
|
|
324
|
+
%i[limit offset].each do |scope|
|
|
325
|
+
regulate_scope(scope) {}
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
InternalMetadata.do_not_synchronize if defined? InternalMetadata
|
|
331
|
+
end
|