activr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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