volt 0.8.27.beta3 → 0.8.27.beta4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +11 -0
  4. data/CONTRIBUTING.md +3 -2
  5. data/{Readme.md → README.md} +9 -12
  6. data/Rakefile +2 -9
  7. data/VERSION +1 -1
  8. data/app/volt/models/user.rb +8 -0
  9. data/app/volt/tasks/live_query/data_store.rb +13 -5
  10. data/app/volt/tasks/live_query/live_query.rb +45 -3
  11. data/app/volt/tasks/live_query/live_query_pool.rb +9 -1
  12. data/app/volt/tasks/query_tasks.rb +20 -2
  13. data/app/volt/tasks/store_tasks.rb +37 -20
  14. data/app/volt/tasks/user_tasks.rb +15 -13
  15. data/lib/volt/boot.rb +5 -3
  16. data/lib/volt/cli/console.rb +1 -0
  17. data/lib/volt/cli/generate.rb +15 -0
  18. data/lib/volt/cli.rb +19 -12
  19. data/lib/volt/config.rb +1 -1
  20. data/lib/volt/controllers/model_controller.rb +13 -3
  21. data/lib/volt/extra_core/extra_core.rb +1 -0
  22. data/lib/volt/extra_core/hash.rb +26 -0
  23. data/lib/volt/extra_core/object.rb +5 -1
  24. data/lib/volt/models/array_model.rb +86 -35
  25. data/lib/volt/models/associations.rb +53 -0
  26. data/lib/volt/models/buffer.rb +22 -10
  27. data/lib/volt/models/dirty.rb +88 -0
  28. data/lib/volt/models/errors.rb +21 -0
  29. data/lib/volt/models/field_helpers.rb +2 -2
  30. data/lib/volt/models/listener_tracker.rb +17 -0
  31. data/lib/volt/models/model.rb +213 -69
  32. data/lib/volt/models/model_helpers.rb +27 -17
  33. data/lib/volt/models/permissions.rb +246 -0
  34. data/lib/volt/models/persistors/array_store.rb +149 -81
  35. data/lib/volt/models/persistors/base.rb +16 -0
  36. data/lib/volt/models/persistors/cookies.rb +14 -9
  37. data/lib/volt/models/persistors/flash.rb +3 -0
  38. data/lib/volt/models/persistors/local_store.rb +0 -16
  39. data/lib/volt/models/persistors/model_store.rb +1 -2
  40. data/lib/volt/models/persistors/query/normalizer.rb +51 -0
  41. data/lib/volt/models/persistors/query/query_listener.rb +21 -5
  42. data/lib/volt/models/persistors/query/query_listener_pool.rb +0 -9
  43. data/lib/volt/models/persistors/store.rb +8 -0
  44. data/lib/volt/models/persistors/store_state.rb +4 -27
  45. data/lib/volt/models/state_helpers.rb +11 -0
  46. data/lib/volt/models/state_manager.rb +43 -0
  47. data/lib/volt/models/url.rb +5 -5
  48. data/lib/volt/models/validations.rb +38 -41
  49. data/lib/volt/models/validators/email_validator.rb +4 -9
  50. data/lib/volt/models/validators/format_validator.rb +23 -8
  51. data/lib/volt/models/validators/length_validator.rb +2 -2
  52. data/lib/volt/models/validators/numericality_validator.rb +7 -3
  53. data/lib/volt/models/validators/phone_number_validator.rb +4 -9
  54. data/lib/volt/models/validators/presence_validator.rb +2 -2
  55. data/lib/volt/models/validators/unique_validator.rb +2 -2
  56. data/lib/volt/models/validators/user_validation.rb +6 -0
  57. data/lib/volt/models.rb +8 -3
  58. data/lib/volt/page/bindings/attribute_binding.rb +10 -4
  59. data/lib/volt/page/bindings/content_binding.rb +9 -5
  60. data/lib/volt/page/bindings/if_binding.rb +25 -2
  61. data/lib/volt/page/bindings/template_binding.rb +19 -1
  62. data/lib/volt/page/bindings/yield_binding.rb +31 -0
  63. data/lib/volt/page/page.rb +11 -16
  64. data/lib/volt/reactive/class_eventable.rb +71 -0
  65. data/lib/volt/reactive/computation.rb +79 -10
  66. data/lib/volt/reactive/dependency.rb +27 -8
  67. data/lib/volt/reactive/eventable.rb +36 -22
  68. data/lib/volt/reactive/reactive_array.rb +2 -3
  69. data/lib/volt/reactive/reactive_hash.rb +8 -3
  70. data/lib/volt/router/routes.rb +2 -1
  71. data/lib/volt/server/component_templates.rb +0 -2
  72. data/lib/volt/server/html_parser/component_view_scope.rb +59 -0
  73. data/lib/volt/server/html_parser/view_handler.rb +3 -0
  74. data/lib/volt/server/html_parser/view_parser.rb +1 -0
  75. data/lib/volt/server/html_parser/view_scope.rb +17 -41
  76. data/lib/volt/server/rack/component_paths.rb +1 -10
  77. data/lib/volt/server/rack/index_files.rb +9 -4
  78. data/lib/volt/server/rack/opal_files.rb +22 -14
  79. data/lib/volt/server/rack/quiet_common_logger.rb +1 -1
  80. data/lib/volt/server/socket_connection_handler.rb +4 -0
  81. data/lib/volt/spec/setup.rb +26 -0
  82. data/lib/volt/tasks/dispatcher.rb +11 -0
  83. data/lib/volt/utils/event_counter.rb +29 -0
  84. data/lib/volt/utils/generic_pool.rb +12 -0
  85. data/lib/volt/utils/modes.rb +40 -0
  86. data/lib/volt/utils/promise_patch.rb +66 -0
  87. data/lib/volt/utils/timers.rb +33 -0
  88. data/lib/volt/volt/users.rb +48 -5
  89. data/lib/volt.rb +4 -0
  90. data/spec/apps/kitchen_sink/Gemfile +3 -1
  91. data/spec/apps/kitchen_sink/app/main/config/routes.rb +9 -8
  92. data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +9 -0
  93. data/spec/apps/kitchen_sink/app/main/controllers/yield_component_controller.rb +5 -0
  94. data/spec/apps/kitchen_sink/app/main/views/main/cookie_test.html +1 -1
  95. data/spec/apps/kitchen_sink/app/main/views/main/index.html +1 -1
  96. data/spec/apps/kitchen_sink/app/main/views/main/main.html +2 -1
  97. data/spec/apps/kitchen_sink/app/main/views/main/yield.html +18 -0
  98. data/spec/apps/kitchen_sink/app/main/views/yield-component/index.html +4 -0
  99. data/spec/extra_core/logger_spec.rb +4 -2
  100. data/spec/integration/user_spec.rb +42 -42
  101. data/spec/integration/yield_spec.rb +18 -0
  102. data/spec/models/associations_spec.rb +37 -0
  103. data/spec/models/dirty_spec.rb +102 -0
  104. data/spec/models/model_spec.rb +64 -8
  105. data/spec/models/model_state_spec.rb +24 -0
  106. data/spec/models/permissions_spec.rb +96 -0
  107. data/spec/models/user_spec.rb +8 -5
  108. data/spec/models/user_validation_spec.rb +24 -0
  109. data/spec/models/validations_spec.rb +44 -5
  110. data/spec/models/validators/email_validator_spec.rb +109 -82
  111. data/spec/models/validators/format_validator_spec.rb +4 -107
  112. data/spec/models/validators/length_validator_spec.rb +9 -9
  113. data/spec/models/validators/phone_number_validator_spec.rb +60 -103
  114. data/spec/models/validators/shared_examples_for_validators.rb +123 -0
  115. data/spec/reactive/class_eventable_spec.rb +37 -0
  116. data/spec/reactive/computation_spec.rb +68 -3
  117. data/spec/reactive/dependency_spec.rb +71 -0
  118. data/spec/reactive/eventable_spec.rb +21 -0
  119. data/spec/reactive/reactive_hash_spec.rb +12 -0
  120. data/spec/router/routes_spec.rb +50 -50
  121. data/spec/server/html_parser/view_parser_spec.rb +0 -3
  122. data/spec/server/rack/component_paths_spec.rb +11 -0
  123. data/spec/server/rack/quite_common_logger_spec.rb +3 -4
  124. data/spec/spec_helper.rb +7 -3
  125. data/templates/component/config/dependencies.rb +1 -7
  126. data/templates/component/config/routes.rb +1 -1
  127. data/templates/component/controllers/main_controller.rb.tt +20 -0
  128. data/templates/component/views/{index → main}/index.html.tt +0 -0
  129. data/templates/newgem/lib/newgem.rb.tt +1 -3
  130. data/templates/project/app/main/config/routes.rb +3 -3
  131. data/templates/project/app/main/views/main/main.html.tt +4 -4
  132. data/templates/project/config/app.rb.tt +6 -0
  133. data/volt.gemspec +11 -7
  134. metadata +96 -42
  135. data/lib/volt/models/model_state.rb +0 -21
  136. data/templates/component/controllers/main_controller.rb +0 -18
@@ -0,0 +1,246 @@
1
+ module Volt
2
+ class Model
3
+
4
+ # The permissions module provides helpers for working with Volt permissions.
5
+ module Permissions
6
+ module ClassMethods
7
+ # Own by user requires a logged in user (Volt.user) to save a model. If
8
+ # the user is not logged in, an validation error will occur. Once created
9
+ # the user can not be changed.
10
+ #
11
+ # @param key [Symbol] the name of the attribute to store
12
+ def own_by_user(key=:user_id)
13
+ # When the model is created, assign it the user_id (if the user is logged in)
14
+ on(:new) do
15
+ if !(user_id = Volt.user_id).nil?
16
+ send(:"_#{key}=", user_id)
17
+ end
18
+ end
19
+
20
+ on(:create, :update) do
21
+ # Don't allow the key to be changed
22
+ deny(key)
23
+ end
24
+
25
+ # Setup a validation that requires a user_id
26
+ validate do
27
+ # Lookup directly in @attributes to optimize and prevent the need
28
+ # for a nil model.
29
+ unless @attributes[:user_id]
30
+ # Show an error that the user is not logged in
31
+ next {key => ['requires a logged in user']}
32
+ end
33
+ end
34
+ end
35
+
36
+
37
+ # TODO: Change to
38
+ # permissions(:create, :read, :update) do |action|
39
+ # if owner?
40
+ # allow
41
+ # else
42
+ # deny :user_id
43
+ # end
44
+ # end
45
+
46
+ # permissions takes a block and yields
47
+ def permissions(*actions, &block)
48
+ # Store the permissions block so we can run it in validations
49
+ self.__permissions__ ||= {}
50
+
51
+ # if no action was specified, assume all actions
52
+ actions += [:create, :read, :update, :delete] if actions.size == 0
53
+
54
+ actions.each do |action|
55
+ # Add to an array of proc's for each action
56
+ (self.__permissions__[action] ||= []) << block
57
+ end
58
+
59
+ validate do
60
+ action = new? ? :create : :update
61
+ run_permissions(action)
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.included(base)
67
+ base.send(:extend, ClassMethods)
68
+ base.class_attribute :__permissions__
69
+ end
70
+
71
+ def allow(*fields)
72
+ if @__allow_fields
73
+ if @__allow_fields != true
74
+ if fields.size == 0
75
+ # No field's were passed, this means we deny all
76
+ @__allow_fields = true
77
+ else
78
+ # Fields were specified, add them to the list
79
+ @__allow_fields += fields.map(&:to_sym)
80
+ end
81
+ end
82
+ else
83
+ raise "allow should be called inside of a permissions block"
84
+ end
85
+ end
86
+
87
+ def deny(*fields)
88
+ if @__deny_fields
89
+ if @__deny_fields != true
90
+ if fields.size == 0
91
+ # No field's were passed, this means we deny all
92
+ @__deny_fields = true
93
+ else
94
+ # Fields were specified, add them to the list
95
+ @__deny_fields += fields.map(&:to_sym)
96
+ end
97
+ end
98
+ else
99
+ raise "deny should be called inside of a permissions block"
100
+ end
101
+ end
102
+
103
+ # owner? can be called on a model to check if the currently logged
104
+ # in user (```Volt.user```) is the owner of this instance.
105
+ #
106
+ # @param key [Symbol] the name of the attribute where the user_id is stored
107
+ def owner?(key=:user_id)
108
+ # Lookup the original user_id
109
+ owner_id = was(key) || send(:"_#{key}")
110
+ owner_id != nil && owner_id == Volt.user_id
111
+ end
112
+
113
+ # Returns boolean if the model can be deleted
114
+ def can_delete?
115
+ action_allowed?(:delete)
116
+ end
117
+
118
+ # Checks the read permissions
119
+ def can_read?
120
+ action_allowed?(:read)
121
+ end
122
+
123
+ def can_create?
124
+ action_allowed?(:create)
125
+ end
126
+
127
+ # Checks if any denies are in place for an action (read or delete)
128
+ def action_allowed?(action_name)
129
+ # TODO: this does some unnecessary work
130
+ compute_allow_and_deny(action_name)
131
+
132
+ deny = @__deny_fields == true || (@__deny_fields && @__deny_fields.size > 0)
133
+
134
+ clear_allow_and_deny
135
+
136
+ return !deny
137
+ end
138
+
139
+ # Return the list of allowed fields
140
+ def allow_and_deny_fields(action_name)
141
+ compute_allow_and_deny(action_name)
142
+
143
+ result = [@__allow_fields, @__deny_fields]
144
+
145
+ clear_allow_and_deny
146
+
147
+ return result
148
+ end
149
+
150
+ # Filter fields returns the attributes with any denied or not allowed fields
151
+ # removed based on the current user.
152
+ #
153
+ # Run with Volt.as_user(...) to change the user
154
+ def filtered_attributes
155
+ # Run the read permission check
156
+ allow, deny = allow_and_deny_fields(:read)
157
+
158
+ if allow && allow != true && allow.size > 0
159
+ # always keep id
160
+ allow << :_id
161
+
162
+ # Only keep fields in the allow list
163
+ return @attributes.select {|key| allow.include?(key) }
164
+ elsif deny == true
165
+ # Only keep id
166
+ # TODO: Should this be a full reject?
167
+ return @attributes.reject {|key| key != :_id }
168
+ elsif deny && deny.size > 0
169
+ # Reject any in the deny list
170
+ return @attributes.reject {|key| deny.include?(key) }
171
+ else
172
+ return @attributes
173
+ end
174
+ end
175
+
176
+ private
177
+ def run_permissions(action_name=nil)
178
+ compute_allow_and_deny(action_name)
179
+
180
+ errors = {}
181
+
182
+ if @__allow_fields == true
183
+ # Allow all fields
184
+ elsif @__allow_fields && @__allow_fields.size > 0
185
+ # Deny all not specified in the allow list
186
+ changed_attributes.keys.each do |field_name|
187
+ unless @__allow_fields.include?(field_name)
188
+ add_error_if_changed(errors, field_name)
189
+ end
190
+ end
191
+ end
192
+
193
+ if @__deny_fields == true
194
+ # Don't allow any field changes
195
+ changed_attributes.keys.each do |field_name|
196
+ add_error_if_changed(errors, field_name)
197
+ end
198
+ elsif @__deny_fields
199
+ # Allow all except the denied
200
+ @__deny_fields.each do |field_name|
201
+ if changed?(field_name)
202
+ add_error_if_changed(errors, field_name)
203
+ end
204
+ end
205
+ end
206
+
207
+ clear_allow_and_deny
208
+
209
+ errors
210
+ end
211
+
212
+ def clear_allow_and_deny
213
+ @__deny_fields = nil
214
+ @__allow_fields = nil
215
+ end
216
+
217
+ # Run through the permission blocks for the action name, acumulate
218
+ # all allow/deny fields.
219
+ def compute_allow_and_deny(action_name)
220
+ @__deny_fields = []
221
+ @__allow_fields = []
222
+
223
+ # Skip permissions can be run on the server to ignore the permissions
224
+ return if Volt.in_mode?(:skip_permissions)
225
+
226
+ # Run the permission blocks
227
+ action_name ||= new? ? :create : :update
228
+
229
+ # Run each of the permission blocks for this action
230
+ permissions = self.class.__permissions__
231
+ if permissions && (blocks = permissions[action_name])
232
+ blocks.each do |block|
233
+ # Call the block, pass the action name
234
+ instance_exec(action_name, &block)
235
+ end
236
+ end
237
+ end
238
+
239
+ def add_error_if_changed(errors, field_name)
240
+ if changed?(field_name)
241
+ (errors[field_name] ||= []) << 'can not be changed'
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -1,6 +1,8 @@
1
1
  require 'volt/models/persistors/store'
2
- require 'volt/models/persistors/query/query_listener_pool'
3
2
  require 'volt/models/persistors/store_state'
3
+ require 'volt/models/persistors/query/normalizer'
4
+ require 'volt/models/persistors/query/query_listener_pool'
5
+ require 'volt/utils/timers'
4
6
 
5
7
  module Volt
6
8
  module Persistors
@@ -9,7 +11,7 @@ module Volt
9
11
 
10
12
  @@query_pool = QueryListenerPool.new
11
13
 
12
- attr_reader :model
14
+ attr_reader :model, :root_dep
13
15
 
14
16
  def self.query_pool
15
17
  @@query_pool
@@ -18,159 +20,225 @@ module Volt
18
20
  def initialize(model, tasks = nil)
19
21
  super
20
22
 
23
+ # The listener event counter keeps track of how many things are listening
24
+ # on this model and loads/unloads data when in use.
25
+ @listener_event_counter = EventCounter.new(
26
+ -> { load_data },
27
+ -> { stop_listening }
28
+ )
29
+
30
+ # The root dependency tracks how many listeners are on the ArrayModel
31
+ # @root_dep = Dependency.new(@listener_event_counter.method(:add), @listener_event_counter.method(:remove))
32
+ @root_dep = Dependency.new(method(:listener_added), method(:listener_removed))
33
+
21
34
  @query = @model.options[:query]
22
- @limit = @model.options[:limit]
23
- @skip = @model.options[:skip]
35
+ end
36
+
37
+ def loaded(initial_state = nil)
38
+ super
39
+
40
+ # Setup up the query listener, and if it is already listening, then
41
+ # go ahead and load that data in. This allows us to use it immediately
42
+ # if the data is loaded in another place.
43
+ if query_listener.listening
44
+ query_listener.add_store(self)
45
+ @added_to_query = true
46
+ end
47
+ end
24
48
 
25
- @skip = nil if @skip == 0
49
+ def inspect
50
+ "<#{self.class.to_s}:#{object_id} #{@model.path.inspect} #{@query.inspect}>"
26
51
  end
27
52
 
53
+ # Called when an each binding is listening
28
54
  def event_added(event, first, first_for_event)
29
55
  # First event, we load the data.
30
56
  if first
31
- @has_events = true
32
- load_data
57
+ @listener_event_counter.add
33
58
  end
34
59
  end
35
60
 
61
+ # Called when an each binding stops listening
36
62
  def event_removed(event, last, last_for_event)
37
63
  # Remove listener where there are no more events on this model
38
64
  if last
39
- @has_events = false
40
- stop_listening
65
+ @listener_event_counter.remove
41
66
  end
42
67
  end
43
68
 
44
- # Called when an event is removed and we no longer want to keep in
45
- # sync with the database.
46
- def stop_listening(stop_watching_query = true)
47
- return if @has_events
48
- return if @fetch_promises && @fetch_promises.size > 0
69
+ # Called by child models to track their listeners
70
+ def listener_added
71
+ @listener_event_counter.add
72
+ # puts "LIST ADDED: #{inspect} - #{@listener_event_counter.count} #{@model.path.inspect}"
73
+ end
49
74
 
50
- @query_computation.stop if @query_computation && stop_watching_query
75
+ # Called by child models to track their listeners
76
+ def listener_removed
77
+ @listener_event_counter.remove
78
+ # puts "LIST REMOVED: #{inspect} - #{@query.inspect} - #{@listener_event_counter.count} #{@model.path.inspect}"
79
+ end
51
80
 
52
- if @query_listener
53
- @query_listener.remove_store(self)
54
- @query_listener = nil
81
+ # Called when an event is removed and we no longer want to keep in
82
+ # sync with the database. The data is kept in memory and the model's
83
+ # loaded_state is marked as "dirty" meaning it may not be in sync.
84
+ def stop_listening
85
+ # puts "Stop LIST1"
86
+ Timers.next_tick do
87
+ Computation.run_without_tracking do
88
+ # puts "STOP LIST2"
89
+ if @listener_event_counter.count == 0
90
+ if @added_to_query
91
+ @query_listener.remove_store(self)
92
+ @query_listener = nil
93
+
94
+ @added_to_query = nil
95
+ end
96
+
97
+ @model.change_state_to(:loaded_state, :dirty)
98
+ end
99
+ end
55
100
  end
56
101
 
57
- @state = :dirty
102
+ Timers.flush_next_tick_timers! if Volt.server?
58
103
  end
59
104
 
60
105
  # Called the first time data is requested from this collection
61
106
  def load_data
62
- # Don't load data from any queried
63
- if @state == :not_loaded || @state == :dirty
64
- # puts "Load Data at #{@model.path.inspect} - query: #{@query.inspect} on #{self.inspect}"
65
- change_state_to :loading
66
-
67
- if @query.is_a?(Proc)
68
- @query_computation = -> do
69
- stop_listening(false)
107
+ # puts "LOAD DATA: #{@model.path.inspect}: #{@model.options[:query].inspect}"
108
+ Computation.run_without_tracking do
109
+ loaded_state = @model.loaded_state
70
110
 
71
- change_state_to :loading
111
+ # Don't load data from any queried
112
+ if loaded_state == :not_loaded || loaded_state == :dirty
113
+ @model.change_state_to(:loaded_state, :loading)
72
114
 
73
- new_query = @query.call
74
-
75
- run_query(@model, @query.call, @skip, @limit)
76
- end.watch!
77
- else
78
- run_query(@model, @query, @skip, @limit)
115
+ run_query
79
116
  end
80
117
  end
81
118
  end
82
119
 
83
- # Clear out the models data, since we're not listening anymore.
84
- def unload_data
85
- puts 'Unload Data'
86
- change_state_to :not_loaded
87
- @model.clear
120
+ def run_query
121
+ unless @added_to_query
122
+ @model.clear
123
+
124
+ @added_to_query = true
125
+ query_listener.add_store(self)
126
+ end
88
127
  end
89
128
 
90
- def run_query(model, query = {}, skip = nil, limit = nil)
91
- @model.clear
129
+ # Looks up the query listener for this ArrayStore
130
+ # @query should be treated as immutable.
131
+ def query_listener
132
+ return @query_listener if @query_listener
133
+
134
+ collection = @model.path.last
135
+ query = @query
92
136
 
93
- collection = model.path.last
94
137
  # Scope to the parent
95
- if model.path.size > 1
96
- parent = model.parent
138
+ if @model.path.size > 1
139
+ parent = @model.parent
97
140
 
98
141
  parent.persistor.ensure_setup if parent.persistor
99
142
 
100
143
  if parent && (attrs = parent.attributes) && attrs[:_id].true?
101
- query[:"#{model.path[-3].singularize}_id"] = attrs[:_id]
144
+ query = query.dup
145
+
146
+ query << [:find, {:"#{@model.path[-3].singularize}_id" => attrs[:_id]}]
102
147
  end
103
148
  end
104
149
 
105
- # The full query contains the skip and limit
106
- full_query = [query, skip, limit]
107
- @query_listener = @@query_pool.lookup(collection, full_query) do
150
+ query = Query::Normalizer.normalize(query)
151
+
152
+ @query_listener ||= @@query_pool.lookup(collection, query) do
108
153
  # Create if it does not exist
109
- QueryListener.new(@@query_pool, @tasks, collection, full_query)
154
+ QueryListener.new(@@query_pool, @tasks, collection, query)
110
155
  end
111
156
 
112
- @query_listener.add_store(self)
157
+ # @@query_pool.print
158
+
159
+ @query_listener
113
160
  end
114
161
 
115
- # Find can take either a query object, or a block that returns a query object. Use
116
- # the block style if you need reactive updating queries
117
- def find(query = nil, &block)
118
- # Set a default query if there is no block
119
- if block
120
- if query
121
- fail 'Query should not be passed in to a find if a block is specified'
122
- end
123
- query = block
124
- else
125
- query ||= {}
126
- end
162
+ # Find takes a query object
163
+ def where(query = nil)
164
+ query ||= {}
127
165
 
128
- Cursor.new([], @model.options.merge(query: query))
166
+ add_query_part(:find, query)
129
167
  end
168
+ alias_method :find, :where
130
169
 
131
170
  def limit(limit)
132
- Cursor.new([], @model.options.merge(limit: limit))
171
+ add_query_part(:limit, limit)
133
172
  end
134
173
 
135
174
  def skip(skip)
136
- Cursor.new([], @model.options.merge(skip: skip))
175
+ add_query_part(:skip, skip)
176
+ end
177
+
178
+ # .sort is already a ruby method, so we use order instead
179
+ def order(sort)
180
+ add_query_part(:sort, sort)
181
+ end
182
+
183
+ # Add query part adds a [method_name, *arguments] array to the query.
184
+ # This will then be passed to the backend to run the query.
185
+ #
186
+ # @return [Cursor] a new cursor
187
+ def add_query_part(*args)
188
+ opts = @model.options
189
+ query = opts[:query] ? opts[:query].deep_clone : []
190
+ query << args
191
+
192
+ # Make a new opts hash with changed query
193
+ opts = opts.merge(query: query)
194
+ Cursor.new([], opts)
137
195
  end
138
196
 
139
197
  # Returns a promise that is resolved/rejected when the query is complete. Any
140
198
  # passed block will be passed to the promises then. Then will be passed the model.
141
- def then(&block)
142
- fail 'then must pass a block' unless block
199
+ def fetch(&block)
143
200
  promise = Promise.new
144
201
 
145
- promise = promise.then(&block)
202
+ # Run the block after resolve if a block is passed in
203
+ promise = promise.then(&block) if block
146
204
 
147
- if @state == :loaded
205
+ if @model.loaded_state == :loaded
148
206
  promise.resolve(@model)
149
207
  else
150
- @fetch_promises ||= []
151
- @fetch_promises << promise
208
+ Proc.new do |comp|
209
+ if @model.loaded_state == :loaded
210
+ promise.resolve(@model)
211
+
212
+ comp.stop
213
+ end
152
214
 
153
- load_data
215
+ end.watch!
154
216
  end
155
217
 
156
218
  promise
157
219
  end
158
220
 
221
+ # Alias then for now
222
+ # TODO: Deprecate
223
+ alias_method :then, :fetch
224
+
159
225
  # Called from backend
160
226
  def add(index, data)
161
227
  $loading_models = true
162
228
 
163
- data_id = data['_id'] || data[:_id]
229
+ Model.no_validate do
230
+ data_id = data['_id'] || data[:_id]
164
231
 
165
- # Don't add if the model is already in the ArrayModel
166
- unless @model.array.find { |v| v._id == data_id }
167
- # Find the existing model, or create one
168
- new_model = @@identity_map.find(data_id) do
169
- new_options = @model.options.merge(path: @model.path + [:[]], parent: @model)
170
- @model.new_model(data, new_options, :loaded)
171
- end
232
+ # Don't add if the model is already in the ArrayModel
233
+ unless @model.array.find { |v| v._id == data_id }
234
+ # Find the existing model, or create one
235
+ new_model = @@identity_map.find(data_id) do
236
+ new_options = @model.options.merge(path: @model.path + [:[]], parent: @model)
237
+ @model.new_model(data, new_options, :loaded)
238
+ end
172
239
 
173
- @model.insert(index, new_model)
240
+ @model.insert(index, new_model)
241
+ end
174
242
  end
175
243
 
176
244
  $loading_models = false
@@ -21,6 +21,22 @@ module Volt
21
21
 
22
22
  def event_removed(event, last, last_for_event)
23
23
  end
24
+
25
+ # Find the root for this model
26
+ def root_model
27
+ node = @model
28
+
29
+ loop do
30
+ parent = node.parent
31
+ if parent
32
+ node = parent
33
+ else
34
+ break
35
+ end
36
+ end
37
+
38
+ node
39
+ end
24
40
  end
25
41
  end
26
42
  end
@@ -22,6 +22,7 @@ module Volt
22
22
  end
23
23
 
24
24
  def write_cookie(key, value, options = {})
25
+ options[:path] ||= '/'
25
26
  parts = []
26
27
 
27
28
  parts << `encodeURIComponent(key)`
@@ -29,12 +30,13 @@ module Volt
29
30
  parts << `encodeURIComponent(value)`
30
31
  parts << '; '
31
32
 
33
+ parts << 'path=' << options[:path] << '; ' if options[:path]
32
34
  parts << 'max-age=' << options[:max_age] << '; ' if options[:max_age]
33
- if options[:expires]
34
- expires = options[:expires]
35
+
36
+ if (expires = options[:expires])
35
37
  parts << 'expires=' << `expires.toGMTString()` << '; '
36
38
  end
37
- parts << 'path=' << options[:path] << '; ' if options[:path]
39
+
38
40
  parts << 'domain=' << options[:domain] << '; ' if options[:domain]
39
41
  parts << 'secure' if options[:secure]
40
42
 
@@ -55,12 +57,15 @@ module Volt
55
57
  def loaded(initial_state = nil)
56
58
  # When the main model is first loaded, we pull in the data from the
57
59
  # store if it exists
58
- if !@loaded && @model.path == []
59
- @loaded = true
60
+ if !@cookies_loaded && @model.path == []
61
+ @cookies_loaded = true
60
62
 
61
63
  writing_cookies do
64
+ # Assign directly so we don't trigger the callbacks on the initial load
65
+ attrs = @model.attributes
66
+
62
67
  read_cookies.each_pair do |key, value|
63
- @model.assign_attribute(key, value)
68
+ attrs[key.to_sym] = value
64
69
  end
65
70
  end
66
71
  end
@@ -70,16 +75,16 @@ module Volt
70
75
  def changed(attribute_name)
71
76
  # TODO: Make sure we're only assigning directly, not sub models
72
77
  unless $writing_cookies
73
- value = @model.read_attribute(attribute_name)
78
+ value = @model.get(attribute_name)
74
79
 
75
80
  # Temp, expire in 1 year, going to expand this api
76
- write_cookie(attribute_name, value.to_s, expires: Time.now + (356 * 24 * 60 * 60))
81
+ write_cookie(attribute_name, value.to_s, expires: Time.now + (356 * 24 * 60 * 60), path: '/')
77
82
  end
78
83
  end
79
84
 
80
85
  def removed(attribute_name)
81
86
  writing_cookies do
82
- write_cookie(attribute_name, '', expires: Time.now)
87
+ write_cookie(attribute_name, '', max_age: 0, path: '/')
83
88
  end
84
89
  end
85
90
 
@@ -16,6 +16,9 @@ module Volt
16
16
  }, 5000);
17
17
  `
18
18
  end
19
+
20
+ # Need to return nil to prevent non-opal object return
21
+ nil
19
22
  end
20
23
 
21
24
  def clear_model(model)
@@ -10,22 +10,6 @@ module Volt
10
10
  @model = model
11
11
  end
12
12
 
13
- # Find the root for this model
14
- def root_model
15
- node = @model
16
-
17
- loop do
18
- parent = node.parent
19
- if parent
20
- node = parent
21
- else
22
- break
23
- end
24
- end
25
-
26
- node
27
- end
28
-
29
13
  # Called when a model is added to the collection
30
14
  def added(model, index)
31
15
  root_model.persistor.save_all