hyper-model 0.6.0 → 0.99.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|