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,44 @@
|
|
|
1
|
+
module Hyperloop
|
|
2
|
+
define_setting :public_model_directories, [File.join('app','hyperloop','models'), File.join('app','models','public')]
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
# adds method to get the HyperMesh public column types
|
|
7
|
+
# this works because the public folder is currently required to be eager loaded.
|
|
8
|
+
class Base
|
|
9
|
+
def self.public_columns_hash
|
|
10
|
+
return @public_columns_hash if @public_columns_hash && Rails.env.production?
|
|
11
|
+
files = []
|
|
12
|
+
Hyperloop.public_model_directories.each do |dir|
|
|
13
|
+
dir_length = Rails.root.join(dir).to_s.length + 1
|
|
14
|
+
Dir.glob(Rails.root.join(dir, '**', '*.rb')).each do |file|
|
|
15
|
+
require_dependency(file) # still the file is loaded to make sure for development and test env
|
|
16
|
+
files << file[dir_length..-4]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
@public_columns_hash = {}
|
|
20
|
+
# descendants only works for already loaded models!
|
|
21
|
+
descendants.each do |model|
|
|
22
|
+
if files.include?(model.name.underscore) && model.name.underscore != 'application_record'
|
|
23
|
+
@public_columns_hash[model.name] = model.columns_hash rescue nil # why rescue?
|
|
24
|
+
end
|
|
25
|
+
# begin
|
|
26
|
+
# @public_columns_hash[model.name] = model.columns_hash if model.table_name
|
|
27
|
+
# rescue Exception => e
|
|
28
|
+
# binding.pry
|
|
29
|
+
# @public_columns_hash = nil
|
|
30
|
+
# raise $!, "Could not read 'columns_hash' for #{model}: #{$!}", $!.backtrace
|
|
31
|
+
# end if files.include?(model.name.underscore) && model.name.underscore != 'application_record'
|
|
32
|
+
end
|
|
33
|
+
@public_columns_hash
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.public_columns_hash_as_json
|
|
37
|
+
return @public_columns_hash_json if @public_columns_hash_json && Rails.env.production?
|
|
38
|
+
pch = public_columns_hash
|
|
39
|
+
return @public_columns_hash_json if @prev_public_columns_hash == pch
|
|
40
|
+
@prev_public_columns_hash = pch
|
|
41
|
+
@public_columns_hash_json = pch.to_json
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ReactiveRecord
|
|
2
|
+
# inspection_details is used by client side ActiveRecord::Base
|
|
3
|
+
# runs down the possible states of a backing record and returns
|
|
4
|
+
# the appropriate string. The order of execution is important!
|
|
5
|
+
module BackingRecordInspector
|
|
6
|
+
def inspection_details
|
|
7
|
+
return error_details unless errors.empty?
|
|
8
|
+
return new_details if new?
|
|
9
|
+
return destroyed_details if destroyed
|
|
10
|
+
return loading_details unless @attributes.key? primary_key
|
|
11
|
+
return dirty_details unless changed_attributes.empty?
|
|
12
|
+
"[loaded id: #{id}]"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def error_details
|
|
16
|
+
id_str = "id: #{id} " unless new?
|
|
17
|
+
"[errors #{id_str}#{errors.messages}]"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def new_details
|
|
21
|
+
"[new #{attributes.select { |attr| column_type(attr) }}]"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def destroyed_details
|
|
25
|
+
"[destroyed id: #{id}]"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def loading_details
|
|
29
|
+
"[loading #{vector}]"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def dirty_details
|
|
33
|
+
"[changed id: #{id} #{changes}]"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
module ReactiveRecord
|
|
2
|
+
class Base
|
|
3
|
+
include BackingRecordInspector
|
|
4
|
+
include Setters
|
|
5
|
+
include Getters
|
|
6
|
+
extend LookupTables
|
|
7
|
+
|
|
8
|
+
# Its all about lazy loading. This prevents us from grabbing enormous association collections, or large attributes
|
|
9
|
+
# unless they are explicitly requested.
|
|
10
|
+
|
|
11
|
+
# During prerendering we get each attribute as its requested and fill it in both on the javascript side, as well as
|
|
12
|
+
# remember that the attribute needs to be part of the download to client.
|
|
13
|
+
|
|
14
|
+
# On the client we fill in the record data with empty values (the default value for the attribute,
|
|
15
|
+
# or one element collections) but only as the attribute
|
|
16
|
+
# is requested. Each request queues up a request to get the real data from the server.
|
|
17
|
+
|
|
18
|
+
# The ReactiveRecord class serves two purposes. First it is the unique data corresponding to the last known state of a
|
|
19
|
+
# database record. This means All records matching a specific database record are unique. This is unlike AR but is
|
|
20
|
+
# important both for the lazy loading and also so that when values change react can be informed of the change.
|
|
21
|
+
|
|
22
|
+
# Secondly it serves as name space for all the ReactiveRecord specific methods, so every AR Instance has a ReactiveRecord
|
|
23
|
+
|
|
24
|
+
# Because there is no point in generating a new ar_instance everytime a search is made we cache the first ar_instance created.
|
|
25
|
+
# Its possible however during loading to create a new ar_instances that will in the end point to the same record.
|
|
26
|
+
|
|
27
|
+
# VECTORS... are an important concept. They are the substitute for a primary key before a record is loaded.
|
|
28
|
+
# Vectors have the form [ModelClass, method_call, method_call, method_call...]
|
|
29
|
+
|
|
30
|
+
# Each method call is either a simple method name or an array in the form [method_name, param, param ...]
|
|
31
|
+
# Example [User, [find, 123], todos, active, [due, "1/1/2016"], title]
|
|
32
|
+
# Roughly corresponds to this query: User.find(123).todos.active.due("1/1/2016").select(:title)
|
|
33
|
+
|
|
34
|
+
attr_accessor :ar_instance
|
|
35
|
+
attr_accessor :vector
|
|
36
|
+
attr_accessor :model
|
|
37
|
+
attr_accessor :changed_attributes
|
|
38
|
+
attr_accessor :aggregate_owner
|
|
39
|
+
attr_accessor :aggregate_attribute
|
|
40
|
+
attr_accessor :destroyed
|
|
41
|
+
attr_accessor :updated_during
|
|
42
|
+
attr_accessor :synced_attributes
|
|
43
|
+
attr_accessor :virgin
|
|
44
|
+
attr_reader :attributes
|
|
45
|
+
|
|
46
|
+
# While data is being loaded from the server certain internal behaviors need to change
|
|
47
|
+
# for example all record changes are synced as they happen.
|
|
48
|
+
# This is implemented this way so that the ServerDataCache class can use pure active
|
|
49
|
+
# record methods in its implementation
|
|
50
|
+
|
|
51
|
+
def self.data_loading?
|
|
52
|
+
@data_loading
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def data_loading?
|
|
56
|
+
self.class.data_loading?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.load_data(&block)
|
|
60
|
+
current_data_loading, @data_loading = [@data_loading, true]
|
|
61
|
+
yield
|
|
62
|
+
ensure
|
|
63
|
+
@data_loading = current_data_loading
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.load_from_json(json, target = nil)
|
|
67
|
+
load_data { ServerDataCache.load_from_json(json, target) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.find(model, attrs)
|
|
71
|
+
# will return the unique record with this attribute-value pair
|
|
72
|
+
# value cannot be an association or aggregation
|
|
73
|
+
|
|
74
|
+
# add the inheritance column if this is an STI subclass
|
|
75
|
+
|
|
76
|
+
inher_col = model.inheritance_column
|
|
77
|
+
if inher_col && model < model.base_class && !attrs.key?(inher_col)
|
|
78
|
+
attrs = attrs.merge(inher_col => model.model_name.to_s)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
model = model.base_class
|
|
82
|
+
primary_key = model.primary_key
|
|
83
|
+
|
|
84
|
+
# already have a record with these attribute-value pairs?
|
|
85
|
+
|
|
86
|
+
record =
|
|
87
|
+
if (id_to_find = attrs[primary_key])
|
|
88
|
+
lookup_by_id(model, id_to_find)
|
|
89
|
+
else
|
|
90
|
+
@records[model].detect do |r|
|
|
91
|
+
!attrs.detect { |attr, value| r.synced_attributes[attr] != value }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
unless record
|
|
96
|
+
# if not, and then the record may be loaded, but not have this attribute set yet,
|
|
97
|
+
# so find the id of of record with the attribute-value pair, and see if that is loaded.
|
|
98
|
+
# find_in_db returns nil if we are not prerendering which will force us to create a new record
|
|
99
|
+
# because there is no way of knowing the id.
|
|
100
|
+
if !attrs.key?(primary_key) && (id = find_in_db(model, attrs))
|
|
101
|
+
record = lookup_by_id(model, id) # @records[model].detect { |record| record.id == id}
|
|
102
|
+
attrs = attrs.merge primary_key => id
|
|
103
|
+
end
|
|
104
|
+
# if we don't have a record then create one
|
|
105
|
+
# (record = new(model)).vector = [model, [:find_by, attribute => value]] unless record
|
|
106
|
+
record ||= set_vector_lookup(new(model), [model, [:find_by, attrs]])
|
|
107
|
+
# and set the values
|
|
108
|
+
attrs.each { |attr, value| record.sync_attribute(attr, value) }
|
|
109
|
+
end
|
|
110
|
+
# finally initialize and return the ar_instance
|
|
111
|
+
record.set_ar_instance!
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.new_from_vector(model, aggregate_owner, *vector)
|
|
115
|
+
# this is the equivilent of find but for associations and aggregations
|
|
116
|
+
# because we are not fetching a specific attribute yet, there is NO communication with the
|
|
117
|
+
# server. That only happens during find.
|
|
118
|
+
model = model.base_class
|
|
119
|
+
|
|
120
|
+
# do we already have a record with this vector? If so return it, otherwise make a new one.
|
|
121
|
+
|
|
122
|
+
# record = @records[model].detect { |record| record.vector == vector }
|
|
123
|
+
record = lookup_by_vector(vector)
|
|
124
|
+
unless record
|
|
125
|
+
|
|
126
|
+
record = new model
|
|
127
|
+
set_vector_lookup(record, vector)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
record.set_ar_instance!
|
|
131
|
+
|
|
132
|
+
if aggregate_owner
|
|
133
|
+
record.aggregate_owner = aggregate_owner
|
|
134
|
+
record.aggregate_attribute = vector.last
|
|
135
|
+
aggregate_owner.attributes[vector.last] = record.ar_instance
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
record.ar_instance
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def initialize(model, hash = {}, ar_instance = nil)
|
|
142
|
+
@model = model
|
|
143
|
+
@ar_instance = ar_instance
|
|
144
|
+
@synced_attributes = {}
|
|
145
|
+
@attributes = {}
|
|
146
|
+
@changed_attributes = []
|
|
147
|
+
@virgin = true
|
|
148
|
+
records[model] << self
|
|
149
|
+
Base.set_object_id_lookup(self)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def find(*args)
|
|
153
|
+
self.class.find(*args)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def new_from_vector(*args)
|
|
157
|
+
self.class.new_from_vector(*args)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def primary_key
|
|
161
|
+
@model.primary_key
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def id
|
|
165
|
+
@attributes[primary_key]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def id=(value)
|
|
169
|
+
# value can be nil if we are loading an aggregate otherwise check if it already exists
|
|
170
|
+
# if !(value && (existing_record = records[@model].detect { |record| record.attributes[primary_key] == value}))
|
|
171
|
+
if !(value && (existing_record = Base.lookup_by_id(model, value)))
|
|
172
|
+
@attributes[primary_key] = value
|
|
173
|
+
Base.set_id_lookup(self)
|
|
174
|
+
else
|
|
175
|
+
@ar_instance.instance_variable_set(:@backing_record, existing_record)
|
|
176
|
+
existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
|
|
177
|
+
end
|
|
178
|
+
value
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def changed?(*args)
|
|
182
|
+
if args.count == 0
|
|
183
|
+
React::State.get_state(self, "!CHANGED!")
|
|
184
|
+
!changed_attributes.empty?
|
|
185
|
+
else
|
|
186
|
+
React::State.get_state(self, args[0])
|
|
187
|
+
changed_attributes.include? args[0]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def changed_attributes_and_values
|
|
192
|
+
Hash[changed_attributes.collect do |attr|
|
|
193
|
+
[attr, @attributes[attr]] if column_type(attr)
|
|
194
|
+
end.compact]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def changes
|
|
198
|
+
Hash[changed_attributes.collect do |attr|
|
|
199
|
+
[attr, [@synced_attributes[attr], @attributes[attr]]] if column_type(attr)
|
|
200
|
+
end.compact]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def errors
|
|
204
|
+
@errors ||= ActiveModel::Errors.new(self)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# called when we have a newly created record, to initialize
|
|
208
|
+
# any nil collections to empty arrays. We can do this because
|
|
209
|
+
# if its a brand new record, then any collections that are still
|
|
210
|
+
# nil must not have any children.
|
|
211
|
+
|
|
212
|
+
def initialize_collections
|
|
213
|
+
if (!vector || vector.empty?) && id && id != ''
|
|
214
|
+
Base.set_vector_lookup(self, [@model, [:find_by, @model.primary_key => id]])
|
|
215
|
+
end
|
|
216
|
+
Base.load_data do
|
|
217
|
+
@model.reflect_on_all_associations.each do |assoc|
|
|
218
|
+
next if !assoc.collection? || @attributes[assoc.attribute]
|
|
219
|
+
ar_instance.send("#{assoc.attribute}=", [])
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# sync! now will also initialize any nil collections
|
|
225
|
+
def sync!(hash = {}) # does NOT notify (see saved! for notification)
|
|
226
|
+
# hash.each do |attr, value|
|
|
227
|
+
# @attributes[attr] = convert(attr, value)
|
|
228
|
+
# end
|
|
229
|
+
@synced_attributes = {}
|
|
230
|
+
hash.each { |attr, value| sync_attribute(attr, convert(attr, value)) }
|
|
231
|
+
@changed_attributes = []
|
|
232
|
+
@saving = false
|
|
233
|
+
errors.clear
|
|
234
|
+
# set the vector and clear collections - this only happens when a new record is saved
|
|
235
|
+
initialize_collections if (!vector || vector.empty?) && id && id != ''
|
|
236
|
+
self
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# this keeps the unscoped collection up to date.
|
|
240
|
+
# @destroy_sync and @create_sync prevent multiple insertions
|
|
241
|
+
# to collections that just have a count
|
|
242
|
+
def sync_unscoped_collection!
|
|
243
|
+
if destroyed
|
|
244
|
+
return if @destroy_sync
|
|
245
|
+
@destroy_sync = true
|
|
246
|
+
else
|
|
247
|
+
return if @create_sync
|
|
248
|
+
@create_sync = true
|
|
249
|
+
end
|
|
250
|
+
model.unscoped << ar_instance
|
|
251
|
+
@synced_with_unscoped = !@synced_with_unscoped
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def sync_attribute(attribute, value)
|
|
255
|
+
|
|
256
|
+
@synced_attributes[attribute] = @attributes[attribute] = value
|
|
257
|
+
Base.set_id_lookup(self) if attribute == primary_key
|
|
258
|
+
|
|
259
|
+
#@synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
|
|
260
|
+
|
|
261
|
+
if value.is_a? Collection
|
|
262
|
+
@synced_attributes[attribute] = value.dup_for_sync
|
|
263
|
+
elsif aggregation = model.reflect_on_aggregation(attribute) and (aggregation.klass < ActiveRecord::Base)
|
|
264
|
+
value.backing_record.sync!
|
|
265
|
+
elsif aggregation
|
|
266
|
+
@synced_attributes[attribute] = aggregation.deserialize(aggregation.serialize(value))
|
|
267
|
+
elsif !model.reflect_on_association(attribute)
|
|
268
|
+
@synced_attributes[attribute] = JSON.parse(value.to_json)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
@changed_attributes.delete(attribute)
|
|
272
|
+
value
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# helper so we can tell if model exists. We need this so we can detect
|
|
276
|
+
# if a record has local changes that are out of sync.
|
|
277
|
+
def self.exists?(model, id)
|
|
278
|
+
Base.lookup_by_id(model, id)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def revert
|
|
282
|
+
@changed_attributes.dup.each do |attribute|
|
|
283
|
+
@ar_instance.send("#{attribute}=", @synced_attributes[attribute])
|
|
284
|
+
@attributes.delete(attribute) unless @synced_attributes.key?(attribute)
|
|
285
|
+
end
|
|
286
|
+
@changed_attributes = []
|
|
287
|
+
errors.clear
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def saving!
|
|
291
|
+
React::State.set_state(self, self, :saving) unless data_loading?
|
|
292
|
+
@saving = true
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def errors!(hash)
|
|
296
|
+
notify_waiting_for_save
|
|
297
|
+
errors.clear && return unless hash
|
|
298
|
+
hash.each do |attribute, messages|
|
|
299
|
+
messages.each do |message|
|
|
300
|
+
errors.add(attribute, message)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def saved!(save_only = nil) # sets saving to false AND notifies
|
|
306
|
+
notify_waiting_for_save
|
|
307
|
+
return self if save_only
|
|
308
|
+
if errors.empty?
|
|
309
|
+
React::State.set_state(self, self, :saved)
|
|
310
|
+
elsif !data_loading?
|
|
311
|
+
React::State.set_state(self, self, :error)
|
|
312
|
+
end
|
|
313
|
+
self
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def self.when_not_saving(model, &block)
|
|
317
|
+
if @records[model].detect(&:saving?)
|
|
318
|
+
wait_for_save(model, &block)
|
|
319
|
+
else
|
|
320
|
+
yield model
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def notify_waiting_for_save
|
|
325
|
+
@saving = false
|
|
326
|
+
self.class.notify_waiting_for_save(model)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def self.notify_waiting_for_save(model)
|
|
330
|
+
waiters = waiting_for_save(model)
|
|
331
|
+
return if waiters.empty? || @records[model].detect(&:saving?)
|
|
332
|
+
waiters.each { |waiter| waiter.call model }
|
|
333
|
+
clear_waiting_for_save(model)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def saving?
|
|
337
|
+
React::State.get_state(self, self)
|
|
338
|
+
@saving
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def new?
|
|
342
|
+
!id && !vector
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def set_ar_instance!
|
|
346
|
+
klass = self.class.infer_type_from_hash(model, @attributes)
|
|
347
|
+
@ar_instance = klass._new_without_sti_type_cast(self) unless @ar_instance.class == klass
|
|
348
|
+
@ar_instance
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
class << self
|
|
352
|
+
def infer_type_from_hash(klass, hash)
|
|
353
|
+
klass = klass.base_class
|
|
354
|
+
return klass unless hash
|
|
355
|
+
type = hash[klass.inheritance_column]
|
|
356
|
+
begin
|
|
357
|
+
return Object.const_get(type)
|
|
358
|
+
rescue Exception => e
|
|
359
|
+
message = "Could not subclass #{klass} as #{type}. Perhaps #{type} class has not been required. Exception: #{e}"
|
|
360
|
+
`console.error(#{message})`
|
|
361
|
+
end unless !type || type == ''
|
|
362
|
+
klass
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
attr_reader :outer_scopes
|
|
366
|
+
|
|
367
|
+
def default_scope
|
|
368
|
+
@class_scopes[:default_scope]
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def unscoped
|
|
372
|
+
@class_scopes[:unscoped]
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def add_to_outer_scopes(item)
|
|
376
|
+
@outer_scopes << item
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# While evaluating scopes we want to catch any requests
|
|
380
|
+
# to the server. Once we catch any requests to the server
|
|
381
|
+
# then all the further scopes in that chain will be made
|
|
382
|
+
# at the server.
|
|
383
|
+
|
|
384
|
+
class DbRequestMade < RuntimeError; end
|
|
385
|
+
|
|
386
|
+
def catch_db_requests(return_val = nil)
|
|
387
|
+
@catch_db_requests = true
|
|
388
|
+
yield
|
|
389
|
+
rescue DbRequestMade => e
|
|
390
|
+
React::IsomorphicHelpers.log "Warning: request for server side data during scope evaluation: #{e.message}", :warning
|
|
391
|
+
return_val
|
|
392
|
+
ensure
|
|
393
|
+
@catch_db_requests = false
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
alias pre_synchromesh_load_from_db load_from_db
|
|
397
|
+
|
|
398
|
+
def load_from_db(*args)
|
|
399
|
+
raise DbRequestMade, args if @catch_db_requests
|
|
400
|
+
pre_synchromesh_load_from_db(*args)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def destroy_associations
|
|
405
|
+
@destroyed = false
|
|
406
|
+
model.reflect_on_all_associations.each do |association|
|
|
407
|
+
if association.collection?
|
|
408
|
+
@attributes[association.attribute].replace([]) if @attributes[association.attribute]
|
|
409
|
+
else
|
|
410
|
+
@ar_instance.send("#{association.attribute}=", nil)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
@destroyed = true
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|