reactive-record 0.7.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.
@@ -0,0 +1,287 @@
1
+ module ReactiveRecord
2
+ class Base
3
+
4
+ # Its all about lazy loading. This prevents us from grabbing enormous association collections, or large attributes
5
+ # unless they are explicitly requested.
6
+
7
+ # During prerendering we get each attribute as its requested and fill it in both on the javascript side, as well as
8
+ # remember that the attribute needs to be part of the download to client.
9
+
10
+ # On the client we fill in the record data with empty values (nil, or one element collections) but only as the attribute
11
+ # is requested. Each request queues up a request to get the real data from the server.
12
+
13
+ # The ReactiveRecord class serves two purposes. First it is the unique data corresponding to the last known state of a
14
+ # database record. This means All records matching a specific database record are unique. This is unlike AR but is
15
+ # important both for the lazy loading and also so that when values change react can be informed of the change.
16
+
17
+ # Secondly it serves as name space for all the ReactiveRecord specific methods, so every AR Instance has a ReactiveRecord
18
+
19
+ # Because there is no point in generating a new ar_instance everytime a search is made we cache the first ar_instance created.
20
+ # Its possible however during loading to create a new ar_instances that will in the end point to the same record.
21
+
22
+ # VECTORS... are an important concept. They are the substitute for a primary key before a record is loaded.
23
+ # Vectors have the form [ModelClass, method_call, method_call, method_call...]
24
+
25
+ # Each method call is either a simple method name or an array in the form [method_name, param, param ...]
26
+ # Example [User, [find, 123], todos, active, [due, "1/1/2016"], title]
27
+ # Roughly corresponds to this query: User.find(123).todos.active.due("1/1/2016").select(:title)
28
+
29
+ attr_accessor :ar_instance
30
+ attr_accessor :vector
31
+ attr_accessor :model
32
+
33
+ # While data is being loaded from the server certain internal behaviors need to change
34
+ # for example records all record changes are synced as they happen.
35
+ # This is implemented this way so that the ServerDataCache class can use pure active
36
+ # record methods in its implementation
37
+
38
+ def self.data_loading?
39
+ @data_loading
40
+ end
41
+
42
+ def data_loading?
43
+ self.class.data_loading?
44
+ end
45
+
46
+ def self.load_from_json(json, target = nil)
47
+ @data_loading = true
48
+ ServerDataCache.load_from_json(json, target)
49
+ @data_loading = false
50
+ end
51
+
52
+ def self.find(model, attribute, value)
53
+
54
+ # will return the unique record with this attribute-value pair
55
+ # value cannot be an association or aggregation
56
+
57
+ model = model.base_class
58
+ # already have a record with this attribute-value pair?
59
+ record = @records[model].detect { |record| record.attributes[attribute] == value}
60
+
61
+ unless record
62
+ # if not, and then the record may be loaded, but not have this attribute set yet,
63
+ # so find the id of of record with the attribute-value pair, and see if that is loaded.
64
+ # find_in_db returns nil if we are not prerendering which will force us to create a new record
65
+ # because there is no way of knowing the id.
66
+ if attribute != model.primary_key and id = find_in_db(model, attribute, value)
67
+ record = @records[model].detect { |record| record.id == id}
68
+ end
69
+ # if we don't have a record then create one
70
+ (record = new(model)).vector = [model, ["find_by_#{attribute}", value]] unless record
71
+ # and set the value
72
+ record.sync_attribute(attribute, value)
73
+ # and set the primary if we have one
74
+ record.sync_attribute(model.primary_key, id) if id
75
+ end
76
+
77
+ # finally initialize and return the ar_instance
78
+ record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)
79
+ end
80
+
81
+ def self.new_from_vector(model, aggregate_parent, *vector)
82
+ # this is the equivilent of find but for associations and aggregations
83
+ # because we are not fetching a specific attribute yet, there is NO communication with the
84
+ # server. That only happens during find.
85
+
86
+ model = model.base_class
87
+
88
+ # do we already have a record with this vector? If so return it, otherwise make a new one.
89
+
90
+ record = @records[model].detect { |record| record.vector == vector}
91
+ (record = new(model)).vector = vector unless record
92
+ record.ar_instance ||= infer_type_from_hash(model, record.attributes).new(record)
93
+
94
+ end
95
+
96
+ def initialize(model, hash = {}, ar_instance = nil)
97
+ @model = model
98
+ @attributes = hash
99
+ @synced_attributes = {}
100
+ @ar_instance = ar_instance
101
+ records[model] << self
102
+ end
103
+
104
+ def find(*args)
105
+ self.find(*args)
106
+ end
107
+
108
+ def new_from_vector(*args)
109
+ self.class.new_from_vector(*args)
110
+ end
111
+
112
+ def primary_key
113
+ @model.primary_key
114
+ end
115
+
116
+ def id
117
+ attributes[primary_key]
118
+ end
119
+
120
+ def id=(value)
121
+ # we need to keep the id unique
122
+ existing_record = records[@model].detect { |record| record.attributes[primary_key] == value}
123
+ if existing_record
124
+ @ar_instance.instance_eval { @backing_record = existing_record }
125
+ existing_record.attributes.merge!(attributes) { |key, v1, v2| v1 }
126
+ else
127
+ attributes[primary_key] = value
128
+ end
129
+ end
130
+
131
+ def attributes
132
+ @last_access_at = Time.now
133
+ @attributes
134
+ end
135
+
136
+ def reactive_get!(attribute)
137
+ unless @destroyed
138
+ apply_method(attribute) unless @attributes.has_key? attribute
139
+ React::State.get_state(self, attribute) unless data_loading?
140
+ attributes[attribute]
141
+ end
142
+ end
143
+
144
+ def reactive_set!(attribute, value)
145
+ unless @destroyed or attributes[attribute] == value
146
+ if association = @model.reflect_on_association(attribute)
147
+ if association.collection?
148
+ collection = Collection.new(association.klass, @ar_instance, association)
149
+ collection.replace(value || [])
150
+ value = collection
151
+ else
152
+ inverse_of = association.inverse_of
153
+ inverse_association = association.klass.reflect_on_association(inverse_of)
154
+ if inverse_association.collection?
155
+ if !value
156
+ attributes[attribute].attributes[inverse_of].delete(@ar_instance)
157
+ elsif value.attributes[inverse_of]
158
+ value.attributes[inverse_of] << @ar_instance
159
+ else
160
+ value.attributes[inverse_of] = Collection.new(@model, value, inverse_association)
161
+ value.attributes[inverse_of].replace [@ar_instance]
162
+ end
163
+ elsif value
164
+ attributes[attribute].attributes[inverse_of] = nil if attributes[attribute]
165
+ value.attributes[inverse_of] = @ar_instance
166
+ React::State.set_state(value.instance_variable_get(:@backing_record), inverse_of, @ar_instance) unless data_loading?
167
+ else
168
+ attributes[attribute].attributes[inverse_of] = nil
169
+ end
170
+ end
171
+ end
172
+ attributes[attribute] = value
173
+ React::State.set_state(self, attribute, value) unless data_loading?
174
+ end
175
+ value
176
+ end
177
+
178
+ def changed?(*args)
179
+ if args.count == 0
180
+ React::State.get_state(self, self)
181
+ @attributes != @synced_attributes
182
+ else
183
+ key = args[0]
184
+ React::State.get_state(@attributes, key)
185
+ @attributes.has_key?(key) != @synced_attributes.has_key?(key) or @attributes[key] != @synced_attributes[key]
186
+ end
187
+ end
188
+
189
+ def sync!(hash = {})
190
+ @attributes.merge! hash
191
+ @synced_attributes = @attributes.dup
192
+ @synced_attributes.each { |key, value| @synced_attributes[key] = value.dup if value.is_a? Collection }
193
+ @saving = false
194
+ React::State.set_state(self, self, :synced) unless data_loading?
195
+ self
196
+ end
197
+
198
+ def sync_attribute(attribute, value)
199
+ @synced_attributes[attribute] = attributes[attribute] = value
200
+ @synced_attributes[attribute] = value.dup if value.is_a? ReactiveRecord::Collection
201
+ value
202
+ end
203
+
204
+ def revert
205
+ @attributes.each do |attribute, value|
206
+ @ar_instance.send("#{attribute}=", @synced_attributes[attribute])
207
+ end
208
+ @attributes.delete_if { |attribute, value| !@synced_attributes.has_key?(attribute) }
209
+ end
210
+
211
+ def saving!
212
+ React::State.set_state(self, self, :saving) unless data_loading?
213
+ @saving = true
214
+ end
215
+
216
+ def saving?
217
+ React::State.get_state(self, self)
218
+ @saving
219
+ end
220
+
221
+ def find_association(association, id)
222
+
223
+ inverse_of = association.inverse_of
224
+
225
+ instance = if id
226
+ find(association.klass, association.klass.primary_key, id)
227
+ else
228
+ new_from_vector(association.klass, nil, *vector, association.attribute)
229
+ end
230
+
231
+ instance_backing_record_attributes = instance.instance_variable_get(:@backing_record).attributes
232
+ inverse_association = association.klass.reflect_on_association(inverse_of)
233
+
234
+ if inverse_association.collection?
235
+ instance_backing_record_attributes[inverse_of] = if id and id != ""
236
+ Collection.new(@model, instance, inverse_association, association.klass, ["find", id], inverse_of)
237
+ else
238
+ Collection.new(@model, instance, inverse_association, *vector, association.attribute, inverse_of)
239
+ end unless instance_backing_record_attributes[inverse_of]
240
+ instance_backing_record_attributes[inverse_of].replace [@ar_instance]
241
+ else
242
+ instance_backing_record_attributes[inverse_of] = @ar_instance
243
+ end if inverse_of
244
+ instance
245
+ end
246
+
247
+ def apply_method(method)
248
+ # Fills in the value returned by sending "method" to the corresponding server side db instance
249
+ if id or vector
250
+ sync_attribute(
251
+ method,
252
+ if association = @model.reflect_on_association(method)
253
+ if association.collection?
254
+ Collection.new(association.klass, @ar_instance, association, *vector, method)
255
+ else
256
+ find_association(association, (id and id != "" and self.class.fetch_from_db([@model, [:find, id], method, @model.primary_key])))
257
+ end
258
+ elsif aggregation = @model.reflect_on_aggregation(method)
259
+ new_from_vector(aggregation.klass, self, *vector, method)
260
+ elsif id and id != ""
261
+ self.class.fetch_from_db([@model, [:find, id], method]) || self.class.load_from_db(*vector, method)
262
+ else # its a attribute in an aggregate or we are on the client and don't know the id
263
+ self.class.fetch_from_db([*vector, method]) || self.class.load_from_db(*vector, method)
264
+ end
265
+ )
266
+ elsif association = @model.reflect_on_association(method) and association.collection?
267
+ @attributes[method] = Collection.new(association.klass, @ar_instance, association)
268
+ elsif aggregation = @model.reflect_on_aggregation(method)
269
+ @attributes[method] = aggregation.klass.new
270
+ end
271
+ end
272
+
273
+ def self.infer_type_from_hash(klass, hash)
274
+ klass = klass.base_class
275
+ return klass unless hash
276
+ type = hash[klass.inheritance_column]
277
+ begin
278
+ return Object.const_get(type)
279
+ rescue Exeception => e
280
+ message = "Could not subclass #{@model_klass.model_name} as #{type}. Perhaps #{type} class has not been required. Exception: #{e}"
281
+ `console.error(#{message})`
282
+ end if type
283
+ klass
284
+ end
285
+
286
+ end
287
+ end
@@ -0,0 +1,100 @@
1
+ module ReactiveRecord
2
+
3
+ class Collection
4
+
5
+ def initialize(target_klass, owner = nil, association = nil, *vector)
6
+ if association and (association.macro != :has_many or association.klass != target_klass)
7
+ message = "unimplemented association #{owner} :#{association.macro} #{association.attribute}"
8
+ `console.error(#{message})`
9
+ end
10
+ @owner = owner # can be nil if this is an outer most scope
11
+ @association = association
12
+ @target_klass = target_klass
13
+ if owner and !owner.id and !owner.vector
14
+ @synced_collection = @collection = []
15
+ else
16
+ @vector = vector.count == 0 ? [target_klass] : vector
17
+ end
18
+ @scopes = {}
19
+ end
20
+
21
+ def all
22
+ unless @collection
23
+ @collection = []
24
+ if ids = ReactiveRecord::Base.fetch_from_db([*@vector, "*all"])
25
+ ids.each do |id|
26
+ @collection << @target_klass.find_by(@target_klass.primary_key => id)
27
+ end
28
+ else
29
+ ReactiveRecord::Base.load_from_db(*@vector, "*all")
30
+ @collection << ReactiveRecord::Base.new_from_vector(@target_klass, nil, *@vector, "*")
31
+ end
32
+ end
33
+ @collection
34
+ end
35
+
36
+
37
+ def ==(other_collection)
38
+ if @collection
39
+ @collection == other_collection.all
40
+ else
41
+ !other_collection.instance_variable_get(:@collection)
42
+ end
43
+ end
44
+
45
+ def apply_scope(scope)
46
+ # The value returned is another ReactiveRecordCollection with the scope added to the vector
47
+ # no additional action is taken
48
+ @scopes[scope] ||= new(@target_klass, @owner, @association, *vector, scope)
49
+ end
50
+
51
+ def proxy_association
52
+ @association
53
+ end
54
+
55
+
56
+ def <<(item)
57
+ inverse_of = @association.inverse_of
58
+ if @owner and inverse_of = @association.inverse_of
59
+ item.attributes[inverse_of].attributes[@association.attribute].delete(item) if item.attributes[inverse_of] and item.attributes[inverse_of].attributes[@association.attribute]
60
+ item.attributes[inverse_of] = @owner
61
+ backing_record = item.instance_variable_get(:@backing_record)
62
+ React::State.set_state(backing_record, inverse_of, @owner) unless backing_record.data_loading?
63
+ end
64
+ all << item unless all.include? item
65
+ self
66
+ end
67
+
68
+ def replace(new_array)
69
+ return new_array if @collection == new_array
70
+ if @collection
71
+ @collection.dup.each { |item| delete(item) }
72
+ else
73
+ @collection = []
74
+ end
75
+ new_array.each { |item| self << item }
76
+ new_array
77
+ end
78
+
79
+ def delete(item)
80
+ if @owner and inverse_of = @association.inverse_of
81
+ item.attributes[inverse_of] = nil
82
+ backing_record = item.instance_variable_get(:@backing_record)
83
+ React::State.set_state(backing_record, inverse_of, nil) unless backing_record.data_loading?
84
+ end
85
+ all.delete(item)
86
+ end
87
+
88
+ def method_missing(method, *args, &block)
89
+ if [].respond_to? method
90
+ all.send(method, *args, &block)
91
+ elsif @target_klass.respond_to? method
92
+ apply_scope(method)
93
+ else
94
+ super
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -0,0 +1,274 @@
1
+ require 'json'
2
+
3
+ module ReactiveRecord
4
+
5
+ class Base
6
+
7
+ include React::IsomorphicHelpers
8
+
9
+ before_first_mount do
10
+ if RUBY_ENGINE != 'opal'
11
+ @server_data_cache = ReactiveRecord::ServerDataCache.new
12
+ else
13
+ @records = Hash.new { |hash, key| hash[key] = [] }
14
+ if on_opal_client?
15
+ @pending_fetches = []
16
+ @last_fetch_at = Time.now
17
+ JSON.from_object(`window.ReactiveRecordInitialData`).each do |hash|
18
+ load_from_json hash
19
+ end unless `typeof window.ReactiveRecordInitialData === 'undefined'`
20
+ end
21
+ end
22
+ end
23
+
24
+ def records
25
+ self.class.instance_variable_get(:@records)
26
+ end
27
+
28
+ # Prerendering db access (returns nil if on client):
29
+ # at end of prerendering dumps all accessed records in the footer
30
+
31
+ isomorphic_method(:fetch_from_db) do |f, vector|
32
+ # vector must end with either "*all", or be a simple attribute
33
+ f.send_to_server [vector.shift.name, *vector] if RUBY_ENGINE == 'opal'
34
+ f.when_on_server { @server_data_cache[*vector] }
35
+ end
36
+
37
+ isomorphic_method(:find_in_db) do |f, klass, attribute, value|
38
+ f.send_to_server klass.name, attribute, value if RUBY_ENGINE == 'opal'
39
+ f.when_on_server { @server_data_cache[klass, ["find_by_#{attribute}", value], :id] }
40
+ end
41
+
42
+ prerender_footer do
43
+ json = @server_data_cache.as_json.to_json # can this just be to_json?
44
+ @server_data_cache.clear_requests
45
+ path = ::Rails.application.routes.routes.detect { |route| route.app.app == ReactiveRecord::Engine }.path.spec
46
+ "<script type='text/javascript'>\n"+
47
+ "window.ReactiveRecordEnginePath = '#{path}';\n"+
48
+ "if (typeof window.ReactiveRecordInitialData === 'undefined') { window.ReactiveRecordInitialData = [] }\n" +
49
+ "window.ReactiveRecordInitialData.push(#{json})\n"+
50
+ "</script>\n"
51
+ end if RUBY_ENGINE != 'opal'
52
+
53
+ # Client side db access (never called during prerendering):
54
+ # queue up fetches, and at the end of each rendering cycle fetch the records
55
+ # notify that loads are pending
56
+
57
+ def self.load_from_db(*vector)
58
+ # only called from the client side
59
+ # pushes the value of vector onto the a list of vectors that will be loaded from the server when the next
60
+ # rendering cycle completes.
61
+ # takes care of informing react that there are things to load, and schedules the loader to run
62
+ # Note there is no equivilent to find_in_db, because each vector implicitly does a find.
63
+ return "" if data_loading?
64
+ ReactiveRecord.loads_pending!
65
+ ReactiveRecord::WhileLoading.loading! # inform react that the current value is bogus
66
+ @pending_fetches << vector
67
+ schedule_fetch
68
+ ""
69
+ end
70
+
71
+ def self.schedule_fetch
72
+ @fetch_scheduled ||= after(0.001) do
73
+ last_fetch_at = @last_fetch_at
74
+ HTTP.post(`window.ReactiveRecordEnginePath`, payload: {pending_fetches: @pending_fetches.uniq}).then do |response|
75
+ begin
76
+ ReactiveRecord::Base.load_from_json(response.json)
77
+ rescue Exception => e
78
+ message = "Exception raised while loading json from server: #{e}"
79
+ `console.error(#{message})`
80
+ end
81
+ ReactiveRecord.run_blocks_to_load
82
+ ReactiveRecord::WhileLoading.loaded_at last_fetch_at
83
+ end if @pending_fetches.count > 0
84
+ @pending_fetches = []
85
+ @last_fetch_at = Time.now
86
+ @fetch_scheduled = nil
87
+ end
88
+ end
89
+
90
+ def self.get_type_hash(record)
91
+ {record.class.inheritance_column => record[record.class.inheritance_column]}
92
+ end
93
+
94
+ # save records
95
+
96
+ if RUBY_ENGINE == 'opal'
97
+
98
+ def save(&block)
99
+
100
+ if data_loading?
101
+
102
+ sync!
103
+
104
+ elsif changed?
105
+
106
+ # we want to pass not just the model data to save, but also enough information so that on return from the server
107
+ # we can update the models on the client
108
+
109
+ # input
110
+ records_to_process = [self] # list of records to process, will grow as we chase associations
111
+ # outputs
112
+ models = [] # the actual data to save {id: record.object_id, model: record.model.model_name, attributes: changed_attributes}
113
+ associations = [] # {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
114
+ # used to keep track of records that have been processed for effeciency
115
+ backing_records = {self.object_id => self} # for quick lookup of records that have been or will be processed [record.object_id] => record
116
+
117
+ add_new_association = lambda do |record, attribute, assoc_record|
118
+ if assoc_record.changed?
119
+ unless backing_records[assoc_record.object_id]
120
+ records_to_process << assoc_record
121
+ backing_records[assoc_record.object_id] = assoc_record
122
+ end
123
+ associations << {parent_id: record.object_id, attribute: attribute, child_id: assoc_record.object_id}
124
+ end
125
+ end
126
+
127
+ record_index = 0
128
+ while(record_index < records_to_process.count)
129
+ record = records_to_process[record_index]
130
+ output_attributes = {record.model.primary_key => record.id}
131
+ models << {id: record.object_id, model: record.model.model_name, attributes: output_attributes}
132
+ record.attributes.each do |attribute, value|
133
+ if association = record.model.reflect_on_association(attribute)
134
+ if association.collection?
135
+ value.each { |assoc| add_new_association.call record, attribute, assoc.instance_variable_get(:@backing_record) }
136
+ else
137
+ add_new_association.call record, attribute, value.instance_variable_get(:@backing_record)
138
+ end
139
+ elsif record.model.reflect_on_aggregation(attribute)
140
+ add_new_association.call record, attribute, value.instance_variable_get(:@backing_record)
141
+ elsif record.changed?(attribute)
142
+ output_attributes[attribute] = value
143
+ end
144
+ end
145
+ record_index += 1
146
+ end
147
+
148
+ backing_records.each { |id, record| record.saving! }
149
+
150
+ promise = Promise.new
151
+
152
+ HTTP.post(`window.ReactiveRecordEnginePath`+"/save", payload: {models: models, associations: associations}).then do |response|
153
+
154
+ response.json[:saved_models].each do |item|
155
+ internal_id, klass, attributes = item
156
+ backing_records[internal_id].sync!(attributes)
157
+ end
158
+ yield response.json[:success], response.json[:message] if block
159
+ promise.resolve response.json[:success], response.json[:message]
160
+ end
161
+ promise
162
+ end
163
+ end
164
+
165
+ else
166
+
167
+ def self.save_records(models, associations)
168
+
169
+ reactive_records = {}
170
+
171
+ models.each do |model_to_save|
172
+ attributes = model_to_save[:attributes]
173
+ model = Object.const_get(model_to_save[:model])
174
+ id = attributes[model.primary_key] # if we are saving existing model primary key value will be present
175
+ reactive_records[model_to_save[:id]] = if id
176
+ record = model.find(id)
177
+ keys = record.attributes.keys
178
+ attributes.each do |key, value|
179
+ record[key] = value if keys.include? key
180
+ end
181
+ record
182
+ else
183
+ record = model.new
184
+ keys = record.attributes.keys
185
+ attributes.each do |key, value|
186
+ record[key] = value if keys.include? key
187
+ end
188
+ record
189
+ end
190
+ end
191
+
192
+ associations.each do |association|
193
+ begin
194
+ if reactive_records[association[:parent_id]].class.reflect_on_aggregation(association[:attribute].to_sym)
195
+ reactive_records[association[:parent_id]].send("#{association[:attribute]}=", reactive_records[association[:child_id]])
196
+ elsif reactive_records[association[:parent_id]].class.reflect_on_association(association[:attribute].to_sym).collection?
197
+ reactive_records[association[:parent_id]].send("#{association[:attribute]}") << reactive_records[association[:child_id]]
198
+ else
199
+ reactive_records[association[:parent_id]].send("#{association[:attribute]}=", reactive_records[association[:child_id]])
200
+ end
201
+ end
202
+ end if associations
203
+
204
+ saved_models = reactive_records.collect do |reactive_record_id, model|
205
+ unless model.frozen?
206
+ saved = model.save
207
+ [reactive_record_id, model.class.name, model.attributes, saved]
208
+ end
209
+ end.compact
210
+
211
+ {success: true, saved_models: saved_models || []}
212
+
213
+ rescue Exception => e
214
+
215
+ {success: false, saved_models: saved_models || [], message: e.message}
216
+
217
+ end
218
+
219
+ end
220
+
221
+ # destroy records
222
+
223
+ if RUBY_ENGINE == 'opal'
224
+
225
+ def destroy(&block)
226
+
227
+ return if @destroyed
228
+
229
+ model.reflect_on_all_associations.each do |association|
230
+ if association.collection?
231
+ attributes[association.attribute].replace([]) if attributes[association.attribute]
232
+ else
233
+ @ar_instance.send("#{association.attribute}=", nil)
234
+ end
235
+ end
236
+
237
+ promise = Promise.new
238
+
239
+ if id or vector
240
+ HTTP.post(`window.ReactiveRecordEnginePath`+"/destroy", payload: {model: ar_instance.model_name, id: id, vector: vector}).then do |response|
241
+ yield response.json[:success], response.json[:message] if block
242
+ promise.resolve response.json[:success], response.json[:message]
243
+ end
244
+ else
245
+ yield true, nil if block
246
+ promise.resolve true, nil
247
+ end
248
+
249
+ @attributes = {}
250
+ sync!
251
+ @destroyed = true
252
+
253
+ promise
254
+ end
255
+
256
+ else
257
+
258
+ def self.destroy_record(model, id, vector)
259
+ model = Object.const_get(model)
260
+ record = if id
261
+ model.find(id)
262
+ else
263
+ ServerDataCache.new[*vector]
264
+ end
265
+ record.destroy
266
+ {success: true, attributes: {}}
267
+ rescue Exception => e
268
+ {success: false, record: record, message: e.message}
269
+ end
270
+
271
+ end
272
+ end
273
+
274
+ end