activr 1.0.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,645 @@
1
+ require 'uri'
2
+
3
+ begin
4
+ require 'moped'
5
+ rescue LoadError
6
+ begin
7
+ require 'mongo'
8
+ rescue LoadError
9
+ raise "[activr] Can't find any suitable mongodb driver: please install 'mongo' or 'moped' gem"
10
+ end
11
+ end
12
+
13
+ #
14
+ # Generic Mongodb driver
15
+ #
16
+ # This is main interface with the underlying MongobDB driver, which can be either the official `mongo` driver or `moped`, the `mongoid` driver.
17
+ #
18
+ class Activr::Storage::MongoDriver
19
+
20
+ def initialize
21
+ # check settings
22
+ raise "Missing setting :uri in config: #{self.config.inspect}" if self.config[:uri].blank?
23
+
24
+ @collections = { }
25
+
26
+ @kind = if defined?(::Moped)
27
+ if defined?(::Moped::BSON)
28
+ # moped < 2.0.0
29
+ :moped_1
30
+ else
31
+ # moped driver
32
+ :moped
33
+ end
34
+ elsif defined?(::Mongo::MongoClient)
35
+ # mongo ruby driver < 2.0.0
36
+ :mongo
37
+ elsif defined?(::Mongo::Client)
38
+ raise "Sorry, mongo gem >= 2.0 is not supported yet"
39
+ else
40
+ raise "Can't find any suitable mongodb driver: please install 'mongo' or 'moped' gem"
41
+ end
42
+
43
+ # Activr.logger.info("Using mongodb driver: #{@kind}")
44
+
45
+ if @kind == :mongo
46
+ uri = URI.parse(self.config[:uri])
47
+
48
+ @db_name = uri.path[1..-1]
49
+ raise "Missing database name in setting uri: #{config[:uri]}" if @db_name.blank?
50
+ end
51
+ end
52
+
53
+ # MongoDB config
54
+ #
55
+ # @api private
56
+ #
57
+ # @return [hash] Config
58
+ def config
59
+ Activr.config.mongodb
60
+ end
61
+
62
+ # Mongodb connection/session
63
+ #
64
+ # @api private
65
+ #
66
+ # @return [Mongo::MongoClient, Mongo::MongoReplicaSetClient, Moped::Session] Connection handler
67
+ def conn
68
+ @conn ||= begin
69
+ case @kind
70
+ when :moped_1, :moped
71
+ ::Moped::Session.connect(self.config[:uri])
72
+ when :mongo
73
+ ::Mongo::MongoClient.from_uri(self.config[:uri])
74
+ end
75
+ end
76
+ end
77
+
78
+ # Mongodb collection
79
+ #
80
+ # @api private
81
+ #
82
+ # @param col_name [String] Collection name
83
+ # @return [Mongo::Collection, Moped::Collection] Collection handler
84
+ def collection(col_name)
85
+ case @kind
86
+ when :moped_1, :moped
87
+ self.conn[col_name]
88
+ when :mongo
89
+ self.conn.db(@db_name).collection(col_name)
90
+ end
91
+ end
92
+
93
+ # Insert a document into given collection
94
+ #
95
+ # @api private
96
+ #
97
+ # @param col [Mongo::Collection, Moped::Collection] Collection handler
98
+ # @param doc [Hash] Document hash to insert
99
+ # @return [BSON::ObjectId, Moped::BSON::ObjectId] Inserted document id
100
+ def insert(col, doc)
101
+ case @kind
102
+ when :moped_1, :moped
103
+ doc_id = doc[:_id] || doc['_id']
104
+ if doc_id.nil?
105
+ doc_id = case @kind
106
+ when :moped_1
107
+ # Moped < 2.0.0 uses a custom BSON implementation
108
+ ::Moped::BSON::ObjectId.new
109
+ when :moped
110
+ # Moped >= 2.0.0 uses bson gem
111
+ ::BSON::ObjectId.new
112
+ end
113
+
114
+ doc['_id'] = doc_id
115
+ end
116
+
117
+ col.insert(doc)
118
+
119
+ doc_id
120
+ when :mongo
121
+ col.insert(doc)
122
+ end
123
+ end
124
+
125
+ # Find a document by id
126
+ #
127
+ # @api private
128
+ #
129
+ # @param col [Mongo::Collection, Moped::Collection] Collection handler
130
+ # @param selector [Hash] Selector hash
131
+ # @return [Hash, OrderedHash, Nil] Document
132
+ def find_one(col, selector)
133
+ case @kind
134
+ when :moped_1, :moped
135
+ col.find(selector).one
136
+ when :mongo
137
+ col.find_one(selector)
138
+ end
139
+ end
140
+
141
+ # Find documents in given collection
142
+ #
143
+ # @api private
144
+ #
145
+ # @param col [Mongo::Collection, Moped::Collection] Collection handler
146
+ # @param selector [Hash] Selector hash
147
+ # @param limit [Integer] Maximum number of documents to find
148
+ # @param skip [Integer] Number of documents to skip
149
+ # @param sort_field [Symbol,String] The field to use to sort documents in descending order
150
+ # @return [Enumerable] An enumerable on found documents
151
+ def find(col, selector, limit, skip, sort_field = nil)
152
+ case @kind
153
+ when :moped_1, :moped
154
+ result = col.find(selector).skip(skip).limit(limit)
155
+ result.sort(sort_field => -1) if sort_field
156
+ result
157
+ when :mongo
158
+ # compute options hash
159
+ options = {
160
+ :limit => limit,
161
+ :skip => skip,
162
+ }
163
+
164
+ options[:sort] = [ sort_field, ::Mongo::DESCENDING ] if sort_field
165
+
166
+ options[:batch_size] = 100 if (limit > 100)
167
+
168
+ col.find(selector, options)
169
+ end
170
+ end
171
+
172
+ # Count documents in given collection
173
+ #
174
+ # @api private
175
+ #
176
+ # @param col [Mongo::Collection, Moped::Collection] Collection handler
177
+ # @param selector [Hash] Selector hash
178
+ # @return [Integer] Number of documents in collections that satisfy given selector
179
+ def count(col, selector)
180
+ case @kind
181
+ when :moped_1, :moped, :mongo
182
+ col.find(selector).count()
183
+ end
184
+ end
185
+
186
+ # Delete documents in given collection
187
+ #
188
+ # @api private
189
+ #
190
+ # @param col [Mongo::Collection, Moped::Collection] Collection handler
191
+ # @param selector [Hash] Selector hash
192
+ def delete(col, selector)
193
+ case @kind
194
+ when :moped_1, :moped
195
+ col.find(selector).remove_all
196
+ when :mongo
197
+ col.remove(selector)
198
+ end
199
+ end
200
+
201
+ # Add index to given collection
202
+ #
203
+ # @api private
204
+ #
205
+ # @param col [Mongo::Collection, Moped::Collection] Collection handler
206
+ # @param index_spec [Array] Array of [ {String}, {Integer} ] tuplets with {String} being a field to index and {Integer} the order (`-1` of DESC and `1` for ASC)
207
+ # @param options [Hash] Options hash
208
+ # @option options [Boolean] :background Background indexing ? (default: `true`)
209
+ # @option options [Boolean] :sparse Is it a sparse index ? (default: `false`)
210
+ # @return [String] Index created
211
+ def add_index(col, index_spec, options = { })
212
+ options = {
213
+ :background => true,
214
+ :sparse => false,
215
+ }.merge(options)
216
+
217
+ case @kind
218
+ when :moped_1, :moped
219
+ index_spec = index_spec.inject(ActiveSupport::OrderedHash.new) do |memo, field_spec|
220
+ memo[field_spec[0]] = field_spec[1]
221
+ memo
222
+ end
223
+
224
+ col.indexes.create(index_spec, options)
225
+
226
+ index_spec
227
+
228
+ when :mongo
229
+ col.create_index(index_spec, options)
230
+ end
231
+ end
232
+
233
+ # Get all indexes for given collection
234
+ #
235
+ # @api private
236
+ #
237
+ # @param col [Mongo::Collection, Moped::Collection] Collection handler
238
+ # @return [Array<String>] Indexes names
239
+ def indexes(col)
240
+ result = [ ]
241
+
242
+ case @kind
243
+ when :moped_1, :moped
244
+ col.indexes.each do |index_spec|
245
+ result << index_spec["name"]
246
+ end
247
+
248
+ when :mongo
249
+ result = col.index_information.keys
250
+ end
251
+
252
+ result
253
+ end
254
+
255
+ # Drop all indexes for given collection
256
+ #
257
+ # @api private
258
+ #
259
+ # @param col [Mongo::Collection, Moped::Collection] Collection handler
260
+ def drop_indexes(col)
261
+ case @kind
262
+ when :moped_1, :moped
263
+ col.indexes.drop
264
+
265
+ when :mongo
266
+ col.drop_indexes
267
+ end
268
+ end
269
+
270
+ # Get handler for `activities` collection
271
+ #
272
+ # @api private
273
+ #
274
+ # @return [Mongo::Collection, Moped::Collection] Collection handler
275
+ def activity_collection
276
+ @activity_collection ||= begin
277
+ col_name = self.config[:activities_col]
278
+ if col_name.nil?
279
+ col_name = "activities"
280
+ col_name = "#{self.config[:col_prefix]}_#{col_name}" unless self.config[:col_prefix].blank?
281
+ end
282
+
283
+ self.collection(col_name)
284
+ end
285
+ end
286
+
287
+ # Get handler for a `<kind>_timelines` collection
288
+ #
289
+ # @api private
290
+ #
291
+ # @param kind [String] Timeline kind
292
+ # @return [Mongo::Collection, Moped::Collection] Collection handler
293
+ def timeline_collection(kind)
294
+ @timeline_collection ||= { }
295
+ @timeline_collection[kind] ||= begin
296
+ col_name = self.config[:timelines_col]
297
+ if col_name.nil?
298
+ col_name = "#{kind}_timelines"
299
+ col_name = "#{self.config[:col_prefix]}_#{col_name}" unless self.config[:col_prefix].blank?
300
+ end
301
+
302
+ self.collection(col_name)
303
+ end
304
+ end
305
+
306
+
307
+ #
308
+ # Main interface with the Storage
309
+ #
310
+
311
+ # (see Activr::Storage#valid_id?)
312
+ def valid_id?(doc_id)
313
+ case @kind
314
+ when :moped_1
315
+ doc_id.is_a?(String) || doc_id.is_a?(::Moped::BSON::ObjectId)
316
+ when :mongo, :moped
317
+ doc_id.is_a?(String) || doc_id.is_a?(::BSON::ObjectId)
318
+ end
319
+ end
320
+
321
+ # Is it a serialized document id (ie. with format { '$oid' => ... })
322
+ #
323
+ # @return [true,false]
324
+ def serialized_id?(doc_id)
325
+ doc_id.is_a?(Hash) && !doc_id['$oid'].blank?
326
+ end
327
+
328
+ # Unserialize a document id
329
+ #
330
+ # @param doc_id [String,Hash] Document id
331
+ # @return [BSON::ObjectId,Moped::BSON::ObjectId] Unserialized document id
332
+ def unserialize_id(doc_id)
333
+ # get string representation
334
+ doc_id = self.serialized_id?(doc_id) ? doc_id['$oid'] : doc_id
335
+
336
+ if @kind == :moped_1
337
+ # Moped < 2.0.0 uses a custom BSON implementation
338
+ if doc_id.is_a?(::Moped::BSON::ObjectId)
339
+ doc_id
340
+ else
341
+ ::Moped::BSON::ObjectId(doc_id)
342
+ end
343
+ else
344
+ if doc_id.is_a?(::BSON::ObjectId)
345
+ doc_id
346
+ else
347
+ ::BSON::ObjectId.from_string(doc_id)
348
+ end
349
+ end
350
+ end
351
+
352
+
353
+ #
354
+ # Activities
355
+ #
356
+
357
+ # Insert an activity document
358
+ #
359
+ # @api private
360
+ #
361
+ # @param activity_hash [Hash] Activity document to insert
362
+ # @return [BSON::ObjectId, Moped::BSON::ObjectId] Inserted activity id
363
+ def insert_activity(activity_hash)
364
+ self.insert(self.activity_collection, activity_hash)
365
+ end
366
+
367
+ # Find an activity document
368
+ #
369
+ # @api private
370
+ #
371
+ # @param activity_id [BSON::ObjectId, Moped::BSON::ObjectId] The activity id
372
+ # @return [Hash, OrderedHash, Nil] Activity document
373
+ def find_activity(activity_id)
374
+ self.find_one(self.activity_collection, { '_id' => activity_id })
375
+ end
376
+
377
+ # Compute selector for querying `activities` collection
378
+ #
379
+ # @api private
380
+ #
381
+ # @param options [Hash] Options when querying `activities` collection
382
+ # @return [Hash] The computed selector
383
+ def activities_selector(options)
384
+ result = { }
385
+
386
+ # compute selector
387
+ if options[:before]
388
+ result['at'] ||= { }
389
+ result['at']["$lt"] = options[:before]
390
+ end
391
+
392
+ if options[:after]
393
+ result['at'] ||= { }
394
+ result['at']["$gt"] = options[:after]
395
+ end
396
+
397
+ (options[:entities] || { }).each do |name, value|
398
+ result[name.to_s] = value
399
+ end
400
+
401
+ if !options[:only].blank?
402
+ result['kind'] ||= { }
403
+ result['kind']['$in'] = options[:only].map(&:kind)
404
+ end
405
+
406
+ if !options[:except].blank?
407
+ result['kind'] ||= { }
408
+ result['kind']['$nin'] = options[:except].map(&:kind)
409
+ end
410
+
411
+ result
412
+ end
413
+
414
+ # (see Storage#find_activities)
415
+ #
416
+ # @api private
417
+ def find_activities(limit, options = { })
418
+ selector = options[:mongo_selector] || self.activities_selector(options)
419
+
420
+ self.find(self.activity_collection, selector, limit, options[:skip], 'at')
421
+ end
422
+
423
+ # (see Storage#count_activities)
424
+ #
425
+ # @api private
426
+ def count_activities(options = { })
427
+ selector = options[:mongo_selector] || self.activities_selector(options)
428
+
429
+ self.count(self.activity_collection, selector)
430
+ end
431
+
432
+ # (see Storage#delete_activities)
433
+ #
434
+ # @api private
435
+ def delete_activities(options = { })
436
+ selector = options[:mongo_selector] || self.activities_selector(options)
437
+
438
+ self.delete(self.activity_collection, selector)
439
+ end
440
+
441
+ # Add index for activities
442
+ #
443
+ # @api private
444
+ #
445
+ # @param index [String,Array<String>] Field or array of fields
446
+ # @param options [Hash] Options hash
447
+ # @option options (see Activr::Storage::MongoDriver#add_index)
448
+ # @return [String] Index created
449
+ def add_activity_index(index, options = { })
450
+ index = index.is_a?(Array) ? index : [ index ]
451
+ index_spec = index.map{ |field| [ field, 1 ] }
452
+
453
+ self.add_index(self.activity_collection, index_spec, options)
454
+ end
455
+
456
+
457
+ #
458
+ # Timeline entries
459
+ #
460
+
461
+ # Insert a timeline entry document
462
+ #
463
+ # @api private
464
+ #
465
+ # @param timeline_kind [String] Timeline kind
466
+ # @param timeline_entry_hash [Hash] Timeline entry document to insert
467
+ def insert_timeline_entry(timeline_kind, timeline_entry_hash)
468
+ self.insert(self.timeline_collection(timeline_kind), timeline_entry_hash)
469
+ end
470
+
471
+ # Find a timeline entry document
472
+ #
473
+ # @api private
474
+ #
475
+ # @param timeline_kind [String] Timeline kind
476
+ # @param tl_entry_id [BSON::ObjectId, Moped::BSON::ObjectId] Timeline entry document id
477
+ # @return [Hash, OrderedHash, Nil] Timeline entry document
478
+ def find_timeline_entry(timeline_kind, tl_entry_id)
479
+ self.find_one(self.timeline_collection(timeline_kind), { '_id' => tl_entry_id })
480
+ end
481
+
482
+ # Compute selector for querying a `*_timelines` collection
483
+ #
484
+ # @api private
485
+ #
486
+ # @param timeline_kind [String] Timeline kind
487
+ # @param recipient_id [String, BSON::ObjectId, Moped::BSON::ObjectId] Recipient id
488
+ # @param options (see Storage#find_timeline)
489
+ # @return [Hash] The computed selector
490
+ def timeline_selector(timeline_kind, recipient_id, options = { })
491
+ result = { }
492
+
493
+ # compute selector
494
+ result['rcpt'] = recipient_id unless recipient_id.nil?
495
+
496
+ if options[:before]
497
+ result['activity.at'] = { "$lt" => options[:before] }
498
+ end
499
+
500
+ (options[:entities] || { }).each do |name, value|
501
+ result["activity.#{name}"] = value
502
+ end
503
+
504
+ if !options[:only].blank?
505
+ result['$or'] = options[:only].map do |route|
506
+ { 'routing' => route.routing_kind, 'activity.kind' => route.activity_class.kind }
507
+ end
508
+ end
509
+
510
+ result
511
+ end
512
+
513
+ # Find several timeline entry documents
514
+ #
515
+ # @api private
516
+ #
517
+ # @param timeline_kind [String] Timeline kind
518
+ # @param recipient_id [String, BSON::ObjectId, Moped::BSON::ObjectId] Recipient id
519
+ # @param limit [Integer] Max number of entries to find
520
+ # @param options (see Storage#find_timeline)
521
+ # @return [Array<Hash>] An array of timeline entry documents
522
+ def find_timeline_entries(timeline_kind, recipient_id, limit, options = { })
523
+ selector = options[:mongo_selector] || self.timeline_selector(timeline_kind, recipient_id, options)
524
+
525
+ self.find(self.timeline_collection(timeline_kind), selector, limit, options[:skip], 'activity.at')
526
+ end
527
+
528
+ # Count number of timeline entry documents
529
+ #
530
+ # @api private
531
+ #
532
+ # @param timeline_kind [String] Timeline kind
533
+ # @param recipient_id [String, BSON::ObjectId, Moped::BSON::ObjectId] Recipient id
534
+ # @param options (see Storage#count_timeline)
535
+ # @return [Integer] Number of documents in given timeline
536
+ def count_timeline_entries(timeline_kind, recipient_id, options = { })
537
+ selector = options[:mongo_selector] || self.timeline_selector(timeline_kind, recipient_id, options)
538
+
539
+ self.count(self.timeline_collection(timeline_kind), selector)
540
+ end
541
+
542
+ # Delete timeline entry documents
543
+ #
544
+ # @api private
545
+ #
546
+ # WARNING: If recipient_id is `nil` then documents are deleted for ALL recipients
547
+ #
548
+ # @param timeline_kind [String] Timeline kind
549
+ # @param recipient_id [String, BSON::ObjectId, Moped::BSON::ObjectId, nil] Recipient id
550
+ # @param options (see Storage#delete_timeline)
551
+ def delete_timeline_entries(timeline_kind, recipient_id, options = { })
552
+ selector = options[:mongo_selector] || self.timeline_selector(timeline_kind, recipient_id, options)
553
+
554
+ # "end of the world" check
555
+ raise "Deleting everything is not the solution" if selector.blank?
556
+
557
+ self.delete(self.timeline_collection(timeline_kind), selector)
558
+ end
559
+
560
+ # Add index for timeline entries
561
+ #
562
+ # @api private
563
+ #
564
+ # @param timeline_kind [String] Timeline kind
565
+ # @param index [String,Array<String>] Field or array of fields
566
+ # @param options [Hash] Options hash
567
+ # @option options (see Activr::Storage::MongoDriver#add_index)
568
+ # @return [String] Index created
569
+ def add_timeline_index(timeline_kind, index, options = { })
570
+ index = index.is_a?(Array) ? index : [ index ]
571
+ index_spec = index.map{ |field| [ field, 1 ] }
572
+
573
+ self.add_index(self.timeline_collection(timeline_kind), index_spec, options)
574
+ end
575
+
576
+
577
+ #
578
+ # Indexes
579
+ #
580
+
581
+ # (see Storage#create_indexes)
582
+ #
583
+ # @api private
584
+ def create_indexes
585
+ # Create indexes on 'activities' collection for models that includes Activr::Entity::ModelMixin
586
+ #
587
+ # eg: activities
588
+ # [['actor', Mongo::ASCENDING], ['at', Mongo::ASCENDING]]
589
+ # [['album', Mongo::ASCENDING], ['at', Mongo::ASCENDING]]
590
+ # [['picture', Mongo::ASCENDING], ['at', Mongo::ASCENDING]]
591
+ Activr.registry.models.each do |model_class|
592
+ if !model_class.activr_entity_settings[:feed_index]
593
+ # @todo Output a warning to remove the index if it exists
594
+ else
595
+ fields = [ model_class.activr_entity_feed_actual_name.to_s, 'at' ]
596
+
597
+ index_name = self.add_activity_index(fields)
598
+ yield("activity / #{index_name}") if block_given?
599
+ end
600
+ end
601
+
602
+ # Create indexes on '*_timelines' collections for defined timeline classes
603
+ #
604
+ # eg: user_news_feed_timelines
605
+ # [['rcpt', Mongo::ASCENDING], ['activity.at', Mongo::ASCENDING]]
606
+ Activr.registry.timelines.each do |timeline_kind, timeline_class|
607
+ fields = [ 'rcpt', 'activity.at' ]
608
+
609
+ index_name = self.add_timeline_index(timeline_kind, fields)
610
+ yield("#{timeline_kind} timeline / #{index_name}") if block_given?
611
+ end
612
+
613
+ # Create sparse indexes to remove activities and timeline entries when entity is deleted
614
+ #
615
+ # eg: activities
616
+ # [['actor', Mongo::ASCENDING]], :sparse => true
617
+ #
618
+ # eg: user_news_feed_timelines
619
+ # [['activity.actor', Mongo::ASCENDING]], :sparse => true
620
+ # [['activity.album', Mongo::ASCENDING]], :sparse => true
621
+ # [['activity.picture', Mongo::ASCENDING]], :sparse => true
622
+ Activr.registry.models.each do |model_class|
623
+ if model_class.activr_entity_settings[:deletable]
624
+ # create sparse index on `activities`
625
+ Activr.registry.activity_entities_for_model(model_class).each do |entity_name|
626
+ # if entity activity feed is enabled and this is the entity name used to fetch that feed then we can use the existing index...
627
+ if !model_class.activr_entity_settings[:feed_index] || (entity_name != model_class.activr_entity_feed_actual_name)
628
+ # ... else we create an index
629
+ index_name = self.add_activity_index(entity_name.to_s, :sparse => true)
630
+ yield("activity / #{index_name}") if block_given?
631
+ end
632
+ end
633
+
634
+ # create sparse index on timeline classes where that entity can be present
635
+ Activr.registry.timeline_entities_for_model(model_class).each do |timeline_class, entities|
636
+ entities.each do |entity_name|
637
+ index_name = self.add_timeline_index(timeline_class.kind, "activity.#{entity_name}", :sparse => true)
638
+ yield("#{timeline_class.kind} timeline / #{index_name}") if block_given?
639
+ end
640
+ end
641
+ end
642
+ end
643
+ end
644
+
645
+ end # class Storage::MongoDriver