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
|
@@ -0,0 +1,18 @@
|
|
|
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
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module ReactiveRecord
|
|
2
|
+
# Keeps track of the details (client side) of a scope.
|
|
3
|
+
# The main point is to provide knowledge of what models
|
|
4
|
+
# the scope is joined with, and the client side
|
|
5
|
+
# filter proc
|
|
6
|
+
class ScopeDescription
|
|
7
|
+
def initialize(model, name, opts)
|
|
8
|
+
sself = self
|
|
9
|
+
@filter_proc = filter_proc(opts)
|
|
10
|
+
@name = name
|
|
11
|
+
model.singleton_class.send(:define_method, "_#{@name}_synchromesh_scope_description_") do
|
|
12
|
+
sself
|
|
13
|
+
end
|
|
14
|
+
@model = model
|
|
15
|
+
build_joins opts[:joins]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :name
|
|
19
|
+
|
|
20
|
+
def self.find(target_model, name)
|
|
21
|
+
name = name.gsub(/!$/, '')
|
|
22
|
+
target_model.send "_#{name}_synchromesh_scope_description_"
|
|
23
|
+
rescue
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def filter?
|
|
28
|
+
@filter_proc.respond_to?(:call)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def collector?
|
|
32
|
+
@is_collector
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def joins_with?(record)
|
|
36
|
+
@joins.detect do |klass, vector|
|
|
37
|
+
# added klass < record.class to handle STI case... should check to see if this could ever
|
|
38
|
+
# cause a problem. Probably not a problem.
|
|
39
|
+
next unless vector.any?
|
|
40
|
+
(klass == :all || record.class == klass || record.class < klass || klass < record.class)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_joins(klass)
|
|
45
|
+
joins = @joins[klass] if @joins.key? klass
|
|
46
|
+
joins ||= @joins[klass.base_class] if @joins.key?(klass.base_class)
|
|
47
|
+
joins || @joins[:all]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def related_records_for(record)
|
|
51
|
+
ReactiveRecord::Base.catch_db_requests([]) do
|
|
52
|
+
get_joins(record.class).collect do |vector|
|
|
53
|
+
crawl(record, *vector)
|
|
54
|
+
end.flatten.compact
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def filter_records(related_records, args)
|
|
59
|
+
if collector?
|
|
60
|
+
Set.new(related_records.to_a.instance_exec(*args, &@filter_proc))
|
|
61
|
+
else
|
|
62
|
+
Set.new(related_records.select { |r| r.instance_exec(*args, &@filter_proc) })
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# private methods
|
|
67
|
+
|
|
68
|
+
def filter_proc(opts)
|
|
69
|
+
return true unless opts.key?(:client) || opts.key?(:select)
|
|
70
|
+
client_opt = opts[:client] || opts[:select]
|
|
71
|
+
@is_collector = opts.key?(:select)
|
|
72
|
+
return client_opt if !client_opt || client_opt.respond_to?(:call)
|
|
73
|
+
raise 'Scope option :client or :select must be a proc, false, or nil'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build_joins(joins_list)
|
|
77
|
+
if !@filter_proc || joins_list == []
|
|
78
|
+
@joins = { all: [] }
|
|
79
|
+
elsif joins_list.nil?
|
|
80
|
+
klass = @model < ActiveRecord::Base ? @model.base_class : @model
|
|
81
|
+
@joins = { klass => [[]], all: [] }
|
|
82
|
+
elsif joins_list == :all
|
|
83
|
+
@joins = { all: [[]] }
|
|
84
|
+
else
|
|
85
|
+
joins_list = [joins_list] unless joins_list.is_a? Array
|
|
86
|
+
map_joins_path joins_list
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def map_joins_path(paths)
|
|
91
|
+
@joins = Hash.new { |h, k| h[k] = Array.new }.merge(@model => [[]])
|
|
92
|
+
paths.each do |path|
|
|
93
|
+
vector = []
|
|
94
|
+
path.split('.').inject(@model) do |model, attribute|
|
|
95
|
+
association = model.reflect_on_association(attribute)
|
|
96
|
+
raise build_error(path, model, attribute) unless association
|
|
97
|
+
vector = [association.inverse_of, *vector]
|
|
98
|
+
@joins[association.klass] << vector
|
|
99
|
+
association.klass
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_error(path, model, attribute)
|
|
105
|
+
"Could not find joins association '#{model.name}.#{attribute}' "\
|
|
106
|
+
"for '#{path}' while processing scope #{@model.name}.#{@name}."
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def crawl(item, method = nil, *vector)
|
|
110
|
+
if !method && item.is_a?(Collection)
|
|
111
|
+
item.all
|
|
112
|
+
elsif !method
|
|
113
|
+
item
|
|
114
|
+
elsif item.respond_to? :collect
|
|
115
|
+
item.collect { |record| crawl(record.send(method), *vector) }
|
|
116
|
+
else
|
|
117
|
+
crawl(item.send(method), *vector)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
module ReactiveRecord
|
|
3
|
+
|
|
4
|
+
# requested cache items I think is there just so prerendering with multiple components works.
|
|
5
|
+
# because we have to dump the cache after each component render (during prererender) but
|
|
6
|
+
# we want to keep the larger cache alive (is this important???) we keep track of what got added
|
|
7
|
+
# to the cache during this cycle
|
|
8
|
+
|
|
9
|
+
# the point is to collect up a all records needed, with whatever attributes were required + primary key, and inheritance column
|
|
10
|
+
# or get all scope arrays, with the record ids
|
|
11
|
+
|
|
12
|
+
# the incoming vector includes the terminal method
|
|
13
|
+
|
|
14
|
+
# output is a hash tree of the form
|
|
15
|
+
# tree ::= {method => tree | [value]} | method's value is either a nested tree or a single value which is wrapped in array
|
|
16
|
+
# {:id => primary_key_id_value} | if its the id method we leave the array off because we know it must be an int
|
|
17
|
+
# {integer => tree} for collections, each item retrieved will be represented by its id
|
|
18
|
+
#
|
|
19
|
+
# example
|
|
20
|
+
# {
|
|
21
|
+
# "User" => {
|
|
22
|
+
# ["find", 12] => {
|
|
23
|
+
# :id => 12
|
|
24
|
+
# "email" => ["mitch@catprint.com"]
|
|
25
|
+
# "todos" => {
|
|
26
|
+
# "active" => {
|
|
27
|
+
# 123 =>
|
|
28
|
+
# {
|
|
29
|
+
# id: 123,
|
|
30
|
+
# title: ["get fetch_records_from_db done"]
|
|
31
|
+
# },
|
|
32
|
+
# 119 =>
|
|
33
|
+
# {
|
|
34
|
+
# id: 119
|
|
35
|
+
# title: ["go for a swim"]
|
|
36
|
+
# }
|
|
37
|
+
# ]
|
|
38
|
+
# }
|
|
39
|
+
# }
|
|
40
|
+
# }
|
|
41
|
+
# }
|
|
42
|
+
# }
|
|
43
|
+
# }
|
|
44
|
+
|
|
45
|
+
# To build this tree we first fill values for each individual vector, saving all the intermediate data
|
|
46
|
+
# when all these are built we build the above hash structure
|
|
47
|
+
|
|
48
|
+
# basic
|
|
49
|
+
# [Todo, [find, 123], title]
|
|
50
|
+
# -> [[Todo, [find, 123], title], "get fetch_records_from_db done", 123]
|
|
51
|
+
|
|
52
|
+
# [User, [find_by_email, "mitch@catprint.com"], first_name]
|
|
53
|
+
# -> [[User, [find_by_email, "mitch@catprint.com"], first_name], "Mitch", 12]
|
|
54
|
+
|
|
55
|
+
# misses
|
|
56
|
+
# [User, [find_by_email, "foobar@catprint.com"], first_name]
|
|
57
|
+
# nothing is found so nothing is downloaded
|
|
58
|
+
# prerendering may do this
|
|
59
|
+
# [User, [find_by_email, "foobar@catprint.com"]]
|
|
60
|
+
# which will return a cache object whose id is nil, and value is nil
|
|
61
|
+
|
|
62
|
+
# scoped collection
|
|
63
|
+
# [User, [find, 12], todos, active, *, title]
|
|
64
|
+
# -> [[User, [find, 12], todos, active, *, title], "get fetch_records_from_db done", 12, 123]
|
|
65
|
+
# -> [[User, [find, 12], todos, active, *, title], "go for a swim", 12, 119]
|
|
66
|
+
|
|
67
|
+
# collection with nested belongs_to
|
|
68
|
+
# [User, [find, 12], todos, *, team]
|
|
69
|
+
# -> [[User, [find, 12], todos, *, team, name], "developers", 12, 123, 252]
|
|
70
|
+
# [[User, [find, 12], todos, *, team, name], nil, 12, 119] <- no team defined for todo<119> so list ends early
|
|
71
|
+
|
|
72
|
+
# collections that are empty will deliver nothing
|
|
73
|
+
# [User, [find, 13], todos, *, team, name] # no todos for user 13
|
|
74
|
+
# evaluation will get this far: [[User, [find, 13], todos], nil, 13]
|
|
75
|
+
# nothing will match [User, [find, 13], todos, team, name] so nothing will be downloaded
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# aggregate
|
|
79
|
+
# [User, [find, 12], address, zip_code]
|
|
80
|
+
# -> [[User, [find, 12], address, zip_code]], "14622", 12] <- note parent id is returned
|
|
81
|
+
|
|
82
|
+
# aggregate with a belongs_to
|
|
83
|
+
# [User, [find, 12], address, country, country_code]
|
|
84
|
+
# -> [[User, [find, 12], address, country, country_code], "US", 12, 342]
|
|
85
|
+
|
|
86
|
+
# collection * (for iterators etc)
|
|
87
|
+
# [User, [find, 12], todos, overdue, *all]
|
|
88
|
+
# -> [[User, [find, 12], todos, active, *all], [119, 123], 12]
|
|
89
|
+
|
|
90
|
+
# [Todo, [find, 119], owner, todos, active, *all]
|
|
91
|
+
# -> [[Todo, [find, 119], owner, todos, active, *all], [119, 123], 119, 12]
|
|
92
|
+
|
|
93
|
+
class ServerDataCache
|
|
94
|
+
|
|
95
|
+
def initialize(acting_user, preloaded_records)
|
|
96
|
+
@acting_user = acting_user
|
|
97
|
+
@cache = []
|
|
98
|
+
@cache_reps = {}
|
|
99
|
+
@requested_cache_items = Set.new
|
|
100
|
+
@preloaded_records = preloaded_records
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
attr_reader :cache
|
|
104
|
+
attr_reader :cache_reps
|
|
105
|
+
attr_reader :requested_cache_items
|
|
106
|
+
|
|
107
|
+
def add_item_to_cache(item)
|
|
108
|
+
cache << item
|
|
109
|
+
cache_reps[item.vector] = item
|
|
110
|
+
requested_cache_items << item
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if RUBY_ENGINE != 'opal'
|
|
114
|
+
|
|
115
|
+
def self.get_model(str)
|
|
116
|
+
# We don't want to open a security hole by allowing some client side string to
|
|
117
|
+
# autoload a class, which would happen if we did a simple str.constantize.
|
|
118
|
+
#
|
|
119
|
+
# Because all AR models are loaded at boot time on the server to define the
|
|
120
|
+
# ActiveRecord::Base.public_columns_hash method any model which the client has
|
|
121
|
+
# access to should already be loaded.
|
|
122
|
+
#
|
|
123
|
+
# If str is not already loaded then we have an access violation.
|
|
124
|
+
unless const_defined? str
|
|
125
|
+
Hyperloop::InternalPolicy.raise_operation_access_violation(:undefined_const, "#{str} is not a loaded constant")
|
|
126
|
+
end
|
|
127
|
+
str.constantize
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def [](*vector)
|
|
131
|
+
timing('building cache_items') do
|
|
132
|
+
root = CacheItem.new(self, @acting_user, vector[0], @preloaded_records)
|
|
133
|
+
vector[1..-1].inject(root) { |cache_item, method| cache_item.apply_method method if cache_item }
|
|
134
|
+
final = vector[1..-1].inject(root) { |cache_item, method| cache_item.apply_method method if cache_item }
|
|
135
|
+
next final unless final && final.value.respond_to?(:superclass) && final.value.superclass <= ActiveRecord::Base
|
|
136
|
+
Hyperloop::InternalPolicy.raise_operation_access_violation(:invalid_vector, "attempt to insecurely access relationship #{vector.last}.")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def start_timing(&block)
|
|
141
|
+
ServerDataCache.start_timing(&block)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def timing(tag, &block)
|
|
145
|
+
ServerDataCache.timing(tag, &block)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.start_timing(&block)
|
|
149
|
+
@timings = Hash.new { |h, k| h[k] = 0 }
|
|
150
|
+
start_time = Time.now
|
|
151
|
+
yield.tap do
|
|
152
|
+
::Rails.logger.debug "********* Total Time #{total = Time.now - start_time} ***********************"
|
|
153
|
+
sum = 0
|
|
154
|
+
@timings.sort_by(&:last).reverse.each do |tag, time|
|
|
155
|
+
::Rails.logger.debug " #{tag}: #{time} (#{(time/total*100).to_i})%"
|
|
156
|
+
sum += time
|
|
157
|
+
end
|
|
158
|
+
::Rails.logger.debug "********* Other Time ***********************"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.timing(tag, &block)
|
|
163
|
+
start_time = Time.now
|
|
164
|
+
tag = tag.to_sym
|
|
165
|
+
yield.tap { @timings[tag] += (Time.now - start_time) if @timings }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.[](models, associations, vectors, acting_user)
|
|
169
|
+
start_timing do
|
|
170
|
+
timing(:public_columns_hash) { ActiveRecord::Base.public_columns_hash }
|
|
171
|
+
result = nil
|
|
172
|
+
ActiveRecord::Base.transaction do
|
|
173
|
+
cache = new(acting_user, timing(:save_records) { ReactiveRecord::Base.save_records(models, associations, acting_user, false, false) })
|
|
174
|
+
timing(:process_vectors) { vectors.each { |vector| cache[*vector] } }
|
|
175
|
+
timing(:as_json) { result = cache.as_json }
|
|
176
|
+
raise ActiveRecord::Rollback, "This Rollback is intentional!"
|
|
177
|
+
end
|
|
178
|
+
result
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def clear_requests
|
|
183
|
+
@requested_cache_items = Set.new
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def as_json
|
|
187
|
+
@requested_cache_items.inject({}) do |hash, cache_item|
|
|
188
|
+
hash.deep_merge! cache_item.as_hash
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def select(&block); @cache.select(&block); end
|
|
193
|
+
|
|
194
|
+
def detect(&block); @cache.detect(&block); end
|
|
195
|
+
|
|
196
|
+
def inject(initial, &block); @cache.inject(initial) █ end
|
|
197
|
+
|
|
198
|
+
class CacheItem
|
|
199
|
+
|
|
200
|
+
attr_reader :vector
|
|
201
|
+
attr_reader :absolute_vector
|
|
202
|
+
attr_reader :root
|
|
203
|
+
attr_reader :acting_user
|
|
204
|
+
|
|
205
|
+
def value
|
|
206
|
+
@value # which is a ActiveRecord object
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def method
|
|
210
|
+
@vector.last
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def self.new(db_cache, acting_user, klass, preloaded_records)
|
|
214
|
+
klass = ServerDataCache.get_model(klass)
|
|
215
|
+
if existing = ServerDataCache.timing(:root_lookup) { db_cache.cache.detect { |cached_item| cached_item.vector == [klass] } }
|
|
216
|
+
return existing
|
|
217
|
+
end
|
|
218
|
+
super
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def initialize(db_cache, acting_user, klass, preloaded_records)
|
|
222
|
+
@db_cache = db_cache
|
|
223
|
+
@acting_user = acting_user
|
|
224
|
+
@vector = @absolute_vector = [klass]
|
|
225
|
+
@value = klass
|
|
226
|
+
@parent = nil
|
|
227
|
+
@root = self
|
|
228
|
+
@preloaded_records = preloaded_records
|
|
229
|
+
@db_cache.add_item_to_cache self
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def start_timing(&block)
|
|
233
|
+
ServerDataCache.class.start_timing(&block)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def timing(tag, &block)
|
|
237
|
+
ServerDataCache.timing(tag, &block)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def apply_method_to_cache(method)
|
|
241
|
+
@db_cache.cache.inject(nil) do |representative, cache_item|
|
|
242
|
+
if cache_item.vector == vector
|
|
243
|
+
if method == "*"
|
|
244
|
+
# apply_star does the security check if value is present
|
|
245
|
+
cache_item.apply_star || representative
|
|
246
|
+
elsif method == "*all"
|
|
247
|
+
# if we secure the collection then we assume its okay to read the ids
|
|
248
|
+
secured_value = cache_item.value.__secure_collection_check(@acting_user)
|
|
249
|
+
cache_item.build_new_cache_item(timing(:active_record) { secured_value.collect { |record| record.id } }, method, method)
|
|
250
|
+
elsif method == "*count"
|
|
251
|
+
secured_value = cache_item.value.__secure_collection_check(@acting_user)
|
|
252
|
+
cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.__secure_collection_check(@acting_user).count }, method, method)
|
|
253
|
+
elsif preloaded_value = @preloaded_records[cache_item.absolute_vector + [method]]
|
|
254
|
+
# no security check needed since we already evaluated this
|
|
255
|
+
cache_item.build_new_cache_item(preloaded_value, method, method)
|
|
256
|
+
elsif aggregation = cache_item.aggregation?(method)
|
|
257
|
+
# aggregations are not protected
|
|
258
|
+
cache_item.build_new_cache_item(aggregation.mapping.collect { |attribute, accessor| cache_item.value[attribute] }, method, method)
|
|
259
|
+
else
|
|
260
|
+
if !cache_item.value || cache_item.value.is_a?(Array)
|
|
261
|
+
# seeing as we just returning representative, no check is needed (its already checked)
|
|
262
|
+
representative
|
|
263
|
+
else
|
|
264
|
+
begin
|
|
265
|
+
secured_method = "__secure_remote_access_to_#{[*method].first}"
|
|
266
|
+
|
|
267
|
+
# order is important. This check must be first since scopes can have same name as attributes!
|
|
268
|
+
if cache_item.value.respond_to? secured_method
|
|
269
|
+
cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.send(secured_method, cache_item.value, @acting_user, *([*method][1..-1])) }, method, method)
|
|
270
|
+
elsif (cache_item.value.class < ActiveRecord::Base) && cache_item.value.attributes.has_key?(method) # TODO: second check is not needed, its built into check_permmissions, check should be does class respond to check_permissions...
|
|
271
|
+
cache_item.value.check_permission_with_acting_user(@acting_user, :view_permitted?, method)
|
|
272
|
+
cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.send(*method) }, method, method)
|
|
273
|
+
else
|
|
274
|
+
raise "method missing"
|
|
275
|
+
end
|
|
276
|
+
rescue Exception => e # this check may no longer be needed as we are quite explicit now on which methods we apply
|
|
277
|
+
# ReactiveRecord::Pry::rescued(e)
|
|
278
|
+
::Rails.logger.debug "\033[0;31;1mERROR: HyperModel exception caught when applying #{method} to db object #{cache_item.value}: #{e}\033[0;30;21m"
|
|
279
|
+
raise e, "HyperModel fetching records failed, exception caught when applying #{method} to db object #{cache_item.value}: #{e}", e.backtrace
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
else
|
|
284
|
+
representative
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def aggregation?(method)
|
|
290
|
+
if method.is_a?(String) && @value.class.respond_to?(:reflect_on_aggregation)
|
|
291
|
+
aggregation = @value.class.reflect_on_aggregation(method.to_sym)
|
|
292
|
+
if aggregation && !(aggregation.klass < ActiveRecord::Base) && @value.send(method)
|
|
293
|
+
aggregation
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def apply_star
|
|
299
|
+
if @value && @value.__secure_collection_check(@acting_user) && @value.length > 0
|
|
300
|
+
i = -1
|
|
301
|
+
@value.inject(nil) do |representative, current_value|
|
|
302
|
+
i += 1
|
|
303
|
+
if preloaded_value = @preloaded_records[@absolute_vector + ["*#{i}"]]
|
|
304
|
+
build_new_cache_item(preloaded_value, "*", "*#{i}")
|
|
305
|
+
else
|
|
306
|
+
build_new_cache_item(current_value, "*", "*#{i}")
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
else
|
|
310
|
+
build_new_cache_item([], "*", "*")
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# TODO replace instance_eval with a method like clone_new_child(....)
|
|
315
|
+
def build_new_cache_item(new_value, method, absolute_method)
|
|
316
|
+
new_parent = self
|
|
317
|
+
self.clone.instance_eval do
|
|
318
|
+
@vector = @vector + [method] # don't push it on since you need a new vector!
|
|
319
|
+
@absolute_vector = @absolute_vector + [absolute_method]
|
|
320
|
+
@value = new_value
|
|
321
|
+
@db_cache.add_item_to_cache self
|
|
322
|
+
@parent = new_parent
|
|
323
|
+
@root = new_parent.root
|
|
324
|
+
self
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def apply_method(method)
|
|
329
|
+
if method.is_a? Array and method.first == "find_by_id"
|
|
330
|
+
method[0] = "find"
|
|
331
|
+
elsif method.is_a? String and method =~ /^\*[0-9]+$/
|
|
332
|
+
method = "*"
|
|
333
|
+
end
|
|
334
|
+
new_vector = vector + [method]
|
|
335
|
+
timing('apply_method lookup') { @db_cache.cache_reps[new_vector] } || apply_method_to_cache(method)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def jsonize(method)
|
|
339
|
+
# sadly standard json converts {[:foo, nil] => 123} to {"['foo', nil]": 123}
|
|
340
|
+
# luckily [:foo, nil] does convert correctly
|
|
341
|
+
# so we check the methods and force proper conversion
|
|
342
|
+
method.is_a?(Array) ? method.to_json : method
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def merge_inheritance_column(children)
|
|
346
|
+
if @value.attributes.key? @value.class.inheritance_column
|
|
347
|
+
children[@value.class.inheritance_column] = [@value[@value.class.inheritance_column]]
|
|
348
|
+
end
|
|
349
|
+
children
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def as_hash(children = nil)
|
|
353
|
+
unless children
|
|
354
|
+
return {} if @value.is_a?(Class) && (@value < ActiveRecord::Base)
|
|
355
|
+
children = [@value.is_a?(BigDecimal) ? @value.to_f : @value]
|
|
356
|
+
end
|
|
357
|
+
if @parent
|
|
358
|
+
if method == "*"
|
|
359
|
+
if @value.is_a? Array # this happens when a scope is empty there is test case, but
|
|
360
|
+
@parent.as_hash({}) # does it work for all edge cases?
|
|
361
|
+
else
|
|
362
|
+
@parent.as_hash({@value.id => children})
|
|
363
|
+
end
|
|
364
|
+
elsif (@value.class < ActiveRecord::Base) && children.is_a?(Hash)
|
|
365
|
+
id = method.is_a?(Array) && method.first == "new" ? [nil] : [@value.id]
|
|
366
|
+
# c = children.merge(id: id)
|
|
367
|
+
# if @value.attributes.key? @value.class.inheritance_column
|
|
368
|
+
# c[@value.class.inheritance_column] = [@value[@value.class.inheritance_column]]
|
|
369
|
+
# end
|
|
370
|
+
@parent.as_hash(jsonize(method) => merge_inheritance_column(children.merge(id: id)))
|
|
371
|
+
elsif method == '*all'
|
|
372
|
+
@parent.as_hash('*all' => children.first)
|
|
373
|
+
else
|
|
374
|
+
@parent.as_hash(jsonize(method) => children)
|
|
375
|
+
end
|
|
376
|
+
else
|
|
377
|
+
{ method.name => children }
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def to_json
|
|
382
|
+
@value.to_json
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
=begin
|
|
390
|
+
tree is a hash, target is the object that will be filled in with the data hanging off the key.
|
|
391
|
+
first time around target == nil, so for each key, value pair we do this: load_from_json(value, Object.const_get(JSON.parse(key)))
|
|
392
|
+
keys:
|
|
393
|
+
':*all': target.replace tree["*all"].collect { |id| target.proxy_association.klass.find(id) }
|
|
394
|
+
Example: {'*all': [1, 7, 19, 23]} target is a collection and will now have 4 records: 1, 7, 19, 23
|
|
395
|
+
|
|
396
|
+
'id': if value is an array then target.id = value.first
|
|
397
|
+
Example: {'id': [17]} Example the target is a record, and its id is now set to 17
|
|
398
|
+
|
|
399
|
+
'*count': target.set_count_state(value.first) note: set_count_state sets the count of a collection and updates the associated state variable
|
|
400
|
+
integer-like-string-or-number: target.push_and_update_belongs_to(key) note: collection will be a has_many association, so we are doing a target << find(key), and updating both ends of the relationship
|
|
401
|
+
[:new, nnn] do a ReactiveRecord::Base.find_by_object_id(target.base_class, method[1]) and that becomes the new target, with val being passed allow_change
|
|
402
|
+
[...] and current target is NOT an ActiveRecord Model (??? a collection ???) then send key to target, and that becomes new target
|
|
403
|
+
but note if value is an array then the scope returned nil, so we destroy the bogus record, and set new target back to nil
|
|
404
|
+
new_target.destroy and new_target = nil if value.is_a? Array
|
|
405
|
+
[...] and current target IS AN ActiveRecord Model (not a collection) then target.backing_record.update_attribute([method], target.backing_record.convert(method, value.first))
|
|
406
|
+
aggregation:
|
|
407
|
+
target.class.respond_to?(:reflect_on_aggregation) and aggregation = target.class.reflect_on_aggregation(method) and !(aggregation.klass < ActiveRecord::Base)
|
|
408
|
+
target.send "#{method}=", aggregation.deserialize(value.first)
|
|
409
|
+
other-string-method-name:
|
|
410
|
+
if value is a an array then value.first is the new value and we do target.send "{key}=", value.first
|
|
411
|
+
if value is a hash
|
|
412
|
+
=end
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def self.load_from_json(tree, target = nil)
|
|
416
|
+
|
|
417
|
+
# have to process *all before any other items
|
|
418
|
+
# we leave the "*all" key in just for debugging purposes, and then skip it below
|
|
419
|
+
|
|
420
|
+
if sorted_collection = tree["*all"]
|
|
421
|
+
target.replace sorted_collection.collect { |id| target.proxy_association.klass.find(id) }
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
if id_value = tree["id"] and id_value.is_a? Array
|
|
425
|
+
target.id = id_value.first
|
|
426
|
+
end
|
|
427
|
+
tree.each do |method, value|
|
|
428
|
+
method = JSON.parse(method) rescue method
|
|
429
|
+
new_target = nil
|
|
430
|
+
|
|
431
|
+
if method == "*all"
|
|
432
|
+
next # its already been processed above
|
|
433
|
+
elsif !target
|
|
434
|
+
load_from_json(value, Object.const_get(method))
|
|
435
|
+
elsif method == "*count"
|
|
436
|
+
target.set_count_state(value.first)
|
|
437
|
+
elsif method.is_a? Integer or method =~ /^[0-9]+$/
|
|
438
|
+
new_target = target.push_and_update_belongs_to(method)
|
|
439
|
+
#target << (new_target = target.proxy_association.klass.find(method))
|
|
440
|
+
elsif method.is_a? Array
|
|
441
|
+
if method[0] == "new"
|
|
442
|
+
new_target = ReactiveRecord::Base.lookup_by_object_id(method[1])
|
|
443
|
+
elsif !(target.class < ActiveRecord::Base)
|
|
444
|
+
new_target = target.send(*method)
|
|
445
|
+
# value is an array if scope returns nil, so we destroy the bogus record
|
|
446
|
+
new_target.destroy and new_target = nil if value.is_a? Array
|
|
447
|
+
else
|
|
448
|
+
target.backing_record.update_simple_attribute([method], target.backing_record.convert(method, value.first))
|
|
449
|
+
end
|
|
450
|
+
elsif target.class.respond_to?(:reflect_on_aggregation) &&
|
|
451
|
+
(aggregation = target.class.reflect_on_aggregation(method)) &&
|
|
452
|
+
!(aggregation.klass < ActiveRecord::Base)
|
|
453
|
+
value = [aggregation.deserialize(value.first)] unless value.first.is_a?(aggregation.klass)
|
|
454
|
+
|
|
455
|
+
target.send "#{method}=", value.first
|
|
456
|
+
elsif value.is_a? Array
|
|
457
|
+
# we cannot use target.send "#{method}=" here because it might be a server method, which does not have a setter
|
|
458
|
+
# a better fix might be something like target._internal_attribute_hash[method] = ...
|
|
459
|
+
target.backing_record.set_attr_value(method, value.first) unless method == :id
|
|
460
|
+
elsif value.is_a? Hash and value[:id] and value[:id].first and association = target.class.reflect_on_association(method)
|
|
461
|
+
# not sure if its necessary to check the id above... is it possible to for the method to be an association but not have an id?
|
|
462
|
+
new_target = association.klass.find(value[:id].first)
|
|
463
|
+
target.send "#{method}=", new_target
|
|
464
|
+
elsif !(target.class < ActiveRecord::Base)
|
|
465
|
+
new_target = target.send(*method)
|
|
466
|
+
# value is an array if scope returns nil, so we destroy the bogus record
|
|
467
|
+
new_target.destroy and new_target = nil if value.is_a? Array
|
|
468
|
+
else
|
|
469
|
+
new_target = target.send("#{method}=", target.send(method))
|
|
470
|
+
end
|
|
471
|
+
load_from_json(value, new_target) if new_target
|
|
472
|
+
end
|
|
473
|
+
rescue Exception => e
|
|
474
|
+
# debugger
|
|
475
|
+
raise e
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|