couchrest_changes 0.1.1 → 0.2.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.
- data/README.md +111 -0
- data/Rakefile +1 -1
- data/lib/couchrest/changes/config.rb +15 -4
- data/lib/couchrest/changes/observer.rb +98 -36
- data/lib/couchrest/changes/version.rb +1 -1
- data/test/config.yaml +3 -5
- data/test/integration/callback_test.rb +69 -12
- data/test/setup_couchdb.rb +85 -0
- data/test/support/integration_helper.rb +30 -0
- data/test/test.netrc +3 -0
- data/test/test_helper.rb +9 -2
- metadata +18 -16
- data/Readme.md +0 -75
- data/test/setup_couch.sh +0 -13
- data/test/support/integration_test.rb +0 -39
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
CouchRest::Changes - keeping track of changes to your couch
|
2
|
+
------------------------------------------------------------
|
3
|
+
|
4
|
+
``CouchRest::Changes`` let's you observe a couch database for changes and react
|
5
|
+
upon them.
|
6
|
+
|
7
|
+
Following the changes of a couch is as easy as
|
8
|
+
```ruby
|
9
|
+
users = CouchRest::Changes.new('users')
|
10
|
+
```
|
11
|
+
|
12
|
+
Callbacks can be defined in blocks:
|
13
|
+
```ruby
|
14
|
+
users.created do |hash|
|
15
|
+
puts "A new user was created with the id: #{hash[:id]}"
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
To start listening just call
|
20
|
+
```ruby
|
21
|
+
users.listen
|
22
|
+
```
|
23
|
+
|
24
|
+
This program is copyright 2015 LEAP Encryption Access Project. It distributed
|
25
|
+
under the same license as CouchRest (Apache License, Version 2.0
|
26
|
+
http://www.apache.org/licenses/).
|
27
|
+
|
28
|
+
Installation
|
29
|
+
---------------------
|
30
|
+
|
31
|
+
Just add couchrest_changes to your Gemfile.
|
32
|
+
|
33
|
+
Configuration
|
34
|
+
---------------------
|
35
|
+
|
36
|
+
``couchrest_changes`` can be configured through ``CouchRest::Changes::Config``
|
37
|
+
|
38
|
+
The default options are similar to the ones used by CouchRest::Model:
|
39
|
+
|
40
|
+
```yaml
|
41
|
+
# couch connection configuration
|
42
|
+
connection:
|
43
|
+
protocol: "http"
|
44
|
+
host: "localhost"
|
45
|
+
port: 5984
|
46
|
+
username: ~
|
47
|
+
password: ~
|
48
|
+
prefix: ""
|
49
|
+
suffix: ""
|
50
|
+
netrc: ""
|
51
|
+
|
52
|
+
# directory to store the last processed record sequence
|
53
|
+
# so we can resume after a restart:
|
54
|
+
seq_dir: "/var/run/couch_changes"
|
55
|
+
|
56
|
+
# Configure log_file like this if you want to log to a file instead of syslog:
|
57
|
+
# log_file: "/var/log/couch_changes.log"
|
58
|
+
log_level: debug
|
59
|
+
|
60
|
+
options:
|
61
|
+
your_own_options: "go here"
|
62
|
+
```
|
63
|
+
|
64
|
+
Normally, CouchRest leaks the CouchDB authentication credentials in the
|
65
|
+
process list. If present, the ``netrc`` configuration value will allow
|
66
|
+
CouchRest::Changes to use the netrc file and keep the credentials out of the
|
67
|
+
process list.
|
68
|
+
|
69
|
+
Running Tests
|
70
|
+
------------------------
|
71
|
+
|
72
|
+
In order for the CouchRest::Changes tests to run, a local CouchDB must be
|
73
|
+
running in "admin party" mode. The tests themselves require that there is no
|
74
|
+
admin party, but the tests will correctly disable the admin party (by creating
|
75
|
+
a super admin called 'superadmin' with password 'secret').
|
76
|
+
|
77
|
+
After the tests have run, the admin party is restored. If something goes
|
78
|
+
wrong, you should remove the superadmin line at the end of
|
79
|
+
/etc/couchdb/local.ini and restart couchdb.
|
80
|
+
|
81
|
+
Examples
|
82
|
+
------------------------
|
83
|
+
|
84
|
+
See [tapicero](https://github.com/leapcode/tapicero) for a daemon that uses
|
85
|
+
CouchRest::Changes. Historically CouchRest::Changes was extracted from
|
86
|
+
tapicero.
|
87
|
+
|
88
|
+
Known Issues
|
89
|
+
-------------
|
90
|
+
|
91
|
+
* CouchRest will miss the first change in a continuous feed.
|
92
|
+
https://github.com/couchrest/couchrest/pull/104 has a fix.
|
93
|
+
You might want to monkeypatch it.
|
94
|
+
|
95
|
+
Changes
|
96
|
+
-------------------------
|
97
|
+
|
98
|
+
0.2.0
|
99
|
+
|
100
|
+
UPGRADING: the configuration 'seq_file' is no longer used,
|
101
|
+
but 'seq_dir' is required.
|
102
|
+
|
103
|
+
* hide password and username from process list by monkeypatching couchrest.
|
104
|
+
* support for listening on changes to multiple databases (new sequence file
|
105
|
+
format, so sequence processing will start from scratch when upgrading).
|
106
|
+
* automatically disable admin party when running tests
|
107
|
+
|
108
|
+
0.1.1
|
109
|
+
|
110
|
+
* recover gracefully if couchdb returns an invalid sequence (as bigcouchd does
|
111
|
+
sometimes)
|
data/Rakefile
CHANGED
@@ -7,7 +7,7 @@ module CouchRest
|
|
7
7
|
|
8
8
|
attr_writer :app_name
|
9
9
|
attr_accessor :connection
|
10
|
-
attr_accessor :
|
10
|
+
attr_accessor :seq_dir
|
11
11
|
attr_accessor :log_file
|
12
12
|
attr_writer :log_level
|
13
13
|
attr_accessor :logger
|
@@ -20,6 +20,9 @@ module CouchRest
|
|
20
20
|
file = find_file(file_path)
|
21
21
|
load_config(file)
|
22
22
|
end
|
23
|
+
unless loaded.compact.any?
|
24
|
+
raise ArgumentError.new("Could not find config file")
|
25
|
+
end
|
23
26
|
self.flags ||= []
|
24
27
|
init_logger
|
25
28
|
log_loaded_configs(loaded.compact)
|
@@ -27,13 +30,21 @@ module CouchRest
|
|
27
30
|
return self
|
28
31
|
end
|
29
32
|
|
30
|
-
def couch_host(
|
31
|
-
|
33
|
+
def couch_host(options = nil)
|
34
|
+
if options
|
35
|
+
conf = connection.merge(options)
|
36
|
+
else
|
37
|
+
conf = connection
|
38
|
+
end
|
32
39
|
userinfo = [conf[:username], conf[:password]].compact.join(':')
|
33
40
|
userinfo += '@' unless userinfo.empty?
|
34
41
|
"#{conf[:protocol]}://#{userinfo}#{conf[:host]}:#{conf[:port]}"
|
35
42
|
end
|
36
43
|
|
44
|
+
def couch_host_no_auth
|
45
|
+
couch_host connection.merge({:password => nil, :username => nil})
|
46
|
+
end
|
47
|
+
|
37
48
|
def couch_host_without_password
|
38
49
|
couch_host connection.merge({:password => nil})
|
39
50
|
end
|
@@ -63,7 +74,7 @@ module CouchRest
|
|
63
74
|
end
|
64
75
|
|
65
76
|
def load_config(file_path)
|
66
|
-
return unless file_path
|
77
|
+
return nil unless file_path
|
67
78
|
load_settings YAML.load(File.read(file_path)), file_path
|
68
79
|
return file_path
|
69
80
|
end
|
@@ -1,19 +1,60 @@
|
|
1
1
|
module CouchRest::Changes
|
2
|
+
|
3
|
+
#
|
4
|
+
# CouchRest uses curl for 'streaming' requests
|
5
|
+
# (requests with a block passed to the db).
|
6
|
+
#
|
7
|
+
# Unfortunately, this leaks the username and password in the process list.
|
8
|
+
# We don't want to do this. So, we create two separate CouchRest::Database
|
9
|
+
# instances: one that is for normal requests and one that is used for
|
10
|
+
# streaming requests. The streaming one we hack to use netrc file in order
|
11
|
+
# to keep authentication info out of the process list.
|
12
|
+
#
|
13
|
+
# If no netrc file is configure, then this DatabaseProxy just uses the
|
14
|
+
# regular db.
|
15
|
+
#
|
16
|
+
class DatabaseProxy
|
17
|
+
def initialize(db_name)
|
18
|
+
@db = CouchRest.new(Config.couch_host).database(db_name)
|
19
|
+
unless @db
|
20
|
+
Config.logger.error "Database #{db_name} not found!"
|
21
|
+
raise RuntimeError "Database #{db_name} not found!"
|
22
|
+
end
|
23
|
+
if Config.connection[:netrc] && !Config.connection[:netrc].empty?
|
24
|
+
@db_stream = CouchRest.new(Config.couch_host_no_auth).database(db_name)
|
25
|
+
streamer = @db_stream.instance_variable_get('@streamer') # cheating, not exposed.
|
26
|
+
streamer.default_curl_opts += " --netrc-file \"#{Config.connection[:netrc]}\""
|
27
|
+
else
|
28
|
+
@db_stream = @db
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def changes(*args, &block)
|
33
|
+
if block
|
34
|
+
@db_stream.changes(*args, &block)
|
35
|
+
else
|
36
|
+
@db.changes(*args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
2
41
|
class Observer
|
3
42
|
|
4
43
|
attr_writer :logger
|
44
|
+
attr_reader :since
|
5
45
|
|
6
46
|
def initialize(db_name, options = {})
|
7
|
-
db_name = Config.complete_db_name(db_name)
|
47
|
+
@db_name = Config.complete_db_name(db_name)
|
8
48
|
info "Tracking #{db_name}"
|
9
49
|
debug "Options: #{options.inspect}" if options.keys.any?
|
10
50
|
@options = options
|
11
|
-
|
12
|
-
|
13
|
-
|
51
|
+
@db = DatabaseProxy.new(@db_name)
|
52
|
+
setup_sequence_file(@db_name)
|
53
|
+
unless rerun?
|
54
|
+
@since = read_seq(@db_name)
|
55
|
+
else
|
56
|
+
@since = 0
|
14
57
|
end
|
15
|
-
read_seq(Config.seq_file) unless rerun?
|
16
|
-
check_seq
|
17
58
|
end
|
18
59
|
|
19
60
|
# triggered when a document was newly created
|
@@ -40,24 +81,24 @@ module CouchRest::Changes
|
|
40
81
|
info "listening..."
|
41
82
|
debug "Starting at sequence #{since}"
|
42
83
|
last = nil
|
43
|
-
result = db.changes
|
84
|
+
result = @db.changes(feed_options) do |hash|
|
44
85
|
last = hash
|
45
86
|
@retry_count = 0
|
46
87
|
callbacks(hash) if hash_for_change?(hash)
|
47
|
-
store_seq(hash["seq"])
|
88
|
+
store_seq(@db_name, hash["seq"])
|
48
89
|
end
|
49
90
|
raise EOFError
|
50
91
|
# appearently MultiJson has issues with the end of the couch stream.
|
51
92
|
# So sometimes we get a MultiJson::LoadError instead...
|
52
|
-
rescue MultiJson::LoadError, EOFError, RestClient::ServerBrokeConnection
|
53
|
-
|
54
|
-
debug result.inspect
|
55
|
-
debug last.inspect
|
93
|
+
rescue MultiJson::LoadError, EOFError, RestClient::ServerBrokeConnection => exc
|
94
|
+
error "Couch stream ended - #{exc.class}"
|
95
|
+
debug result.inspect if result
|
96
|
+
debug last.inspect if last
|
56
97
|
retry if retry_without_sequence?(result, last) || retry_later?
|
57
98
|
end
|
58
99
|
|
59
100
|
def last_sequence
|
60
|
-
hash = db.changes :limit => 1, :descending => true
|
101
|
+
hash = @db.changes :limit => 1, :descending => true
|
61
102
|
return hash["last_seq"]
|
62
103
|
end
|
63
104
|
|
@@ -71,10 +112,6 @@ module CouchRest::Changes
|
|
71
112
|
end.merge @options
|
72
113
|
end
|
73
114
|
|
74
|
-
def since
|
75
|
-
@since ||= 0 # last_sequence
|
76
|
-
end
|
77
|
-
|
78
115
|
def callbacks(hash)
|
79
116
|
# let's not track design document changes
|
80
117
|
return if hash['id'].start_with? '_design/'
|
@@ -94,33 +131,57 @@ module CouchRest::Changes
|
|
94
131
|
end
|
95
132
|
end
|
96
133
|
|
97
|
-
|
134
|
+
#
|
135
|
+
# ensure the sequence file exists
|
136
|
+
#
|
137
|
+
def setup_sequence_file(db_name)
|
138
|
+
filename = sequence_file_name(db_name)
|
139
|
+
unless Dir.exists?(Config.seq_dir)
|
140
|
+
FileUtils.mkdir_p(Config.seq_dir)
|
141
|
+
unless Dir.exists?(Config.seq_dir)
|
142
|
+
raise StandardError.new("Can't create sequence directory #{Config.seq_dir}")
|
143
|
+
end
|
144
|
+
end
|
145
|
+
unless File.exists?(filename)
|
146
|
+
FileUtils.touch(filename)
|
147
|
+
unless File.writable?(filename)
|
148
|
+
raise StandardError.new("Can't write to sequence file #{filename}")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# reads the sequence file, e.g. (/var/run/tapicero/users.seq), returning
|
155
|
+
# the sequence number or zero if the sequence number could not be
|
156
|
+
# determined.
|
157
|
+
#
|
158
|
+
def read_seq(db_name)
|
159
|
+
filename = sequence_file_name(db_name)
|
98
160
|
debug "Looking up sequence here: #{filename}"
|
99
|
-
|
100
|
-
|
101
|
-
|
161
|
+
result = File.read(filename)
|
162
|
+
if result.empty?
|
163
|
+
debug "Found no sequence in the file."
|
164
|
+
return 0
|
165
|
+
else
|
166
|
+
debug "Found sequence: #{result}"
|
167
|
+
return result.to_i
|
102
168
|
end
|
103
|
-
@since = File.read(filename)
|
104
169
|
rescue Errno::ENOENT => e
|
105
170
|
warn "No sequence file found. Starting from scratch"
|
171
|
+
return 0
|
106
172
|
end
|
107
173
|
|
108
|
-
def
|
109
|
-
|
110
|
-
@since = nil
|
111
|
-
debug "Found no sequence in the file."
|
112
|
-
elsif @since
|
113
|
-
debug "Found sequence: #{@since}"
|
114
|
-
end
|
174
|
+
def store_seq(db_name, seq)
|
175
|
+
File.write sequence_file_name(db_name), seq.to_i
|
115
176
|
end
|
116
177
|
|
117
|
-
def
|
118
|
-
File.
|
178
|
+
def sequence_file_name(db_name)
|
179
|
+
File.join(Config.seq_dir, db_name + '.seq')
|
119
180
|
end
|
120
181
|
|
121
182
|
def retry_without_sequence?(result, last_hash)
|
122
183
|
if malformated_sequence?(result) || malformated_sequence?(last_hash)
|
123
|
-
@since =
|
184
|
+
@since = 0
|
124
185
|
info "Trying to start from scratch."
|
125
186
|
end
|
126
187
|
end
|
@@ -165,6 +226,11 @@ module CouchRest::Changes
|
|
165
226
|
logger.warn message
|
166
227
|
end
|
167
228
|
|
229
|
+
def error(message)
|
230
|
+
return unless log_attempt?
|
231
|
+
logger.error message
|
232
|
+
end
|
233
|
+
|
168
234
|
# let's not clutter the logs if couch is down for a longer time.
|
169
235
|
def log_attempt?
|
170
236
|
[0, 1, 2, 4, 8, 20, 40, 120].include?(retry_count) ||
|
@@ -179,9 +245,5 @@ module CouchRest::Changes
|
|
179
245
|
logger ||= Config.logger
|
180
246
|
end
|
181
247
|
|
182
|
-
def db
|
183
|
-
@db
|
184
|
-
end
|
185
|
-
|
186
248
|
end
|
187
249
|
end
|
data/test/config.yaml
CHANGED
@@ -7,13 +7,11 @@ connection:
|
|
7
7
|
protocol: "http"
|
8
8
|
host: "localhost"
|
9
9
|
port: 5984
|
10
|
-
username: anna
|
11
|
-
password: secret
|
10
|
+
username: "anna"
|
11
|
+
password: "secret"
|
12
12
|
prefix: "couchrest_changes_test"
|
13
13
|
suffix: ""
|
14
14
|
|
15
|
-
|
16
|
-
# a restart:
|
17
|
-
seq_file: "/tmp/couchrest_changes_test.seq"
|
15
|
+
seq_dir: "/tmp/couchrest_changes"
|
18
16
|
log_file: "/tmp/couchrest_changes_test.log"
|
19
17
|
log_level: debug
|
@@ -1,34 +1,91 @@
|
|
1
|
-
|
1
|
+
require_relative '../test_helper'
|
2
2
|
|
3
3
|
class CallbackTest < CouchRest::Changes::IntegrationTest
|
4
4
|
|
5
5
|
def setup
|
6
6
|
super
|
7
|
+
@config = CouchRest::Changes::Config
|
8
|
+
@config.load BASE_DIR, 'test/config.yaml'
|
7
9
|
@config.flags = ['--run-once']
|
8
|
-
@changes = CouchRest::Changes.new 'records'
|
9
|
-
File.write config.seq_file, @changes.last_sequence
|
10
10
|
end
|
11
11
|
|
12
12
|
def teardown
|
13
|
-
delete
|
14
13
|
end
|
15
14
|
|
16
15
|
def test_triggers_created
|
17
16
|
handler = mock 'handler'
|
18
17
|
handler.expects(:callback).once
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
|
19
|
+
changes = CouchRest::Changes.new 'records'
|
20
|
+
changes.created { handler.callback }
|
21
|
+
File.write sequence_file, changes.last_sequence
|
22
|
+
|
23
|
+
db_connect('records', @config) do |db|
|
24
|
+
db.create_record
|
25
|
+
changes.listen
|
26
|
+
db.delete_record
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# CouchRest::Changes.new will apply whatever values are in
|
32
|
+
# the current Config.
|
33
|
+
#
|
34
|
+
def test_netrc
|
35
|
+
db_connect('records', @config) do |db|
|
36
|
+
#
|
37
|
+
# test with a bad netrc.
|
38
|
+
#
|
39
|
+
# I wish we could test for RestClient::Unauthorized
|
40
|
+
# but CouchRest just silently eats Couch's
|
41
|
+
# {"error"=>"unauthorized"} response and returns as
|
42
|
+
# if there was no problem. Grrr.
|
43
|
+
#
|
44
|
+
handler = mock 'handler'
|
45
|
+
handler.expects(:callback).never
|
46
|
+
@config.connection[:netrc] = "error"
|
47
|
+
changes = CouchRest::Changes.new 'records'
|
48
|
+
File.write sequence_file, changes.last_sequence
|
49
|
+
changes.created {handler.callback}
|
50
|
+
db.create_record
|
51
|
+
changes.listen
|
52
|
+
db.delete_record
|
53
|
+
|
54
|
+
#
|
55
|
+
# now test with a good netrc.
|
56
|
+
#
|
57
|
+
handler = mock 'handler'
|
58
|
+
handler.expects(:callback).once
|
59
|
+
@config.connection[:netrc] = File.expand_path("../../test.netrc", __FILE__)
|
60
|
+
changes = CouchRest::Changes.new 'records'
|
61
|
+
changes.created { handler.callback}
|
62
|
+
File.write sequence_file, changes.last_sequence
|
63
|
+
db.create_record
|
64
|
+
changes.listen
|
65
|
+
db.delete_record
|
66
|
+
end
|
22
67
|
end
|
23
68
|
|
24
69
|
def test_starts_from_scratch_on_invalid_sequence
|
25
|
-
File.write config.seq_file, "invalid string"
|
26
|
-
@changes = CouchRest::Changes.new 'records'
|
27
70
|
handler = mock 'handler'
|
28
71
|
handler.expects(:callback).at_least_once
|
29
|
-
|
30
|
-
|
31
|
-
|
72
|
+
|
73
|
+
File.write sequence_file, "invalid string"
|
74
|
+
changes = CouchRest::Changes.new 'records'
|
75
|
+
changes.created { |hash| handler.callback(hash) }
|
76
|
+
|
77
|
+
db_connect('records', @config) do |db|
|
78
|
+
db.create_record
|
79
|
+
changes.listen
|
80
|
+
db.delete_record
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def sequence_file(db_name='records')
|
87
|
+
name = CouchRest::Changes::Config.complete_db_name(db_name)
|
88
|
+
@config.seq_dir + '/' + name + '.seq'
|
32
89
|
end
|
33
90
|
|
34
91
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
#
|
2
|
+
# Setup CouchDB for testing.
|
3
|
+
#
|
4
|
+
|
5
|
+
TEST_DB_NAME = 'records'
|
6
|
+
|
7
|
+
def save_doc(db, record)
|
8
|
+
id = record["_id"]
|
9
|
+
db.save_doc(record)
|
10
|
+
puts " * created #{db.name}/#{id}"
|
11
|
+
rescue RestClient::Conflict
|
12
|
+
puts " * #{db.name}/#{id} already exists"
|
13
|
+
rescue RestClient::Exception => exc
|
14
|
+
puts " * Error saving #{db.name}/#{id}: #{exc}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def super_host(config)
|
18
|
+
config.couch_host(:username => 'superadmin', :password => 'secret')
|
19
|
+
end
|
20
|
+
|
21
|
+
# remove prior superadmin, if it happens to exist
|
22
|
+
def remove_super_admin(config)
|
23
|
+
begin
|
24
|
+
CouchRest.delete(super_host(config) + "/_config/admins/superadmin")
|
25
|
+
puts " * removed superadmin"
|
26
|
+
rescue RestClient::ResourceNotFound
|
27
|
+
rescue RestClient::Unauthorized
|
28
|
+
rescue RestClient::Exception => exc
|
29
|
+
puts " * Unable to remove superadmin from CouchDB: #{exc}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# add superadmin to remove admin party
|
34
|
+
def create_super_admin(config)
|
35
|
+
begin
|
36
|
+
CouchRest.put(config.couch_host_no_auth + "/_config/admins/superadmin", 'secret')
|
37
|
+
puts " * created superadmin"
|
38
|
+
rescue RestClient::ResourceNotFound
|
39
|
+
rescue RestClient::Exception => exc
|
40
|
+
puts " * Unable to add superadmin from CouchDB: #{exc}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup_couchdb
|
45
|
+
CouchRest::Changes::Config.load(BASE_DIR, 'test/config.yaml').tap do |config|
|
46
|
+
remove_super_admin(config)
|
47
|
+
create_super_admin(config)
|
48
|
+
CouchRest.new(super_host(config)).database('_users').tap do |db|
|
49
|
+
# create unprivileged user
|
50
|
+
save_doc(db, {
|
51
|
+
"_id" => "org.couchdb.user:me",
|
52
|
+
"name" => "me",
|
53
|
+
"roles" => ["normal"],
|
54
|
+
"type" => "user",
|
55
|
+
"password" => "password"
|
56
|
+
})
|
57
|
+
# create privileged user
|
58
|
+
save_doc(db, {
|
59
|
+
"_id" => "org.couchdb.user:anna",
|
60
|
+
"name" => "anna",
|
61
|
+
"roles" => ["admin"],
|
62
|
+
"type" => "user",
|
63
|
+
"password" => "secret"
|
64
|
+
})
|
65
|
+
end
|
66
|
+
CouchRest.new(super_host(config)).database(config.complete_db_name(TEST_DB_NAME)).tap do |db|
|
67
|
+
db.create!
|
68
|
+
puts " * created db #{db}"
|
69
|
+
save_doc(db, {
|
70
|
+
"_id" => "_security",
|
71
|
+
"admins" => {
|
72
|
+
"names" => ["anna"],
|
73
|
+
"roles" => ["admin"]
|
74
|
+
},
|
75
|
+
"members" => {
|
76
|
+
"names" => ["me"],
|
77
|
+
"roles" => ["normal"]
|
78
|
+
}
|
79
|
+
})
|
80
|
+
end
|
81
|
+
Minitest.after_run do
|
82
|
+
remove_super_admin(config)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CouchRest::Changes
|
2
|
+
|
3
|
+
class TestDatabase
|
4
|
+
def initialize(url, db_name)
|
5
|
+
@db = CouchRest.new(url).database(db_name)
|
6
|
+
@record = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_record(fast = false)
|
10
|
+
result = @db.save_doc :some => :content
|
11
|
+
raise RuntimeError.new(result.inspect) unless result['ok']
|
12
|
+
@record = {'_id' => result["id"], '_rev' => result["rev"]}
|
13
|
+
sleep 0.25
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete_record(fast = false)
|
17
|
+
return if @record.nil? or @record['_deleted']
|
18
|
+
result = @db.delete_doc @record
|
19
|
+
raise RuntimeError.new(result.inspect) unless result['ok']
|
20
|
+
@record['_deleted'] = true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class IntegrationTest < MiniTest::Test
|
25
|
+
def db_connect(db_name, config)
|
26
|
+
yield TestDatabase.new(config.couch_host, config.complete_db_name(db_name))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/test/test.netrc
ADDED
data/test/test_helper.rb
CHANGED
@@ -1,11 +1,18 @@
|
|
1
1
|
require 'rubygems'
|
2
|
+
gem 'minitest'
|
2
3
|
require 'minitest/autorun'
|
3
4
|
|
4
5
|
BASE_DIR = File.expand_path('../..', __FILE__)
|
5
6
|
$:.unshift File.expand_path('lib', BASE_DIR)
|
7
|
+
$:.unshift File.dirname(__FILE__)
|
6
8
|
|
7
9
|
require 'couchrest/changes'
|
8
|
-
require 'support/
|
9
|
-
|
10
|
+
require 'support/integration_helper'
|
10
11
|
require 'mocha/setup'
|
12
|
+
require 'setup_couchdb'
|
11
13
|
|
14
|
+
puts
|
15
|
+
puts " SETTING UP COUCHDB FOR TESTS:"
|
16
|
+
puts
|
17
|
+
setup_couchdb()
|
18
|
+
puts
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: couchrest_changes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2015-03-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: couchrest
|
@@ -66,7 +66,7 @@ dependencies:
|
|
66
66
|
requirements:
|
67
67
|
- - ~>
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version:
|
69
|
+
version: 5.4.0
|
70
70
|
type: :development
|
71
71
|
prerelease: false
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -74,7 +74,7 @@ dependencies:
|
|
74
74
|
requirements:
|
75
75
|
- - ~>
|
76
76
|
- !ruby/object:Gem::Version
|
77
|
-
version:
|
77
|
+
version: 5.4.0
|
78
78
|
- !ruby/object:Gem::Dependency
|
79
79
|
name: mocha
|
80
80
|
requirement: !ruby/object:Gem::Requirement
|
@@ -131,17 +131,18 @@ executables: []
|
|
131
131
|
extensions: []
|
132
132
|
extra_rdoc_files: []
|
133
133
|
files:
|
134
|
+
- lib/couchrest/changes.rb
|
134
135
|
- lib/couchrest/changes/config.rb
|
135
|
-
- lib/couchrest/changes/version.rb
|
136
136
|
- lib/couchrest/changes/observer.rb
|
137
|
-
- lib/couchrest/changes.rb
|
137
|
+
- lib/couchrest/changes/version.rb
|
138
138
|
- Rakefile
|
139
|
-
-
|
140
|
-
- test/
|
141
|
-
- test/setup_couch.sh
|
139
|
+
- README.md
|
140
|
+
- test/setup_couchdb.rb
|
142
141
|
- test/config.yaml
|
143
|
-
- test/support/integration_test.rb
|
144
142
|
- test/test_helper.rb
|
143
|
+
- test/support/integration_helper.rb
|
144
|
+
- test/test.netrc
|
145
|
+
- test/integration/callback_test.rb
|
145
146
|
homepage: https://leap.se
|
146
147
|
licenses: []
|
147
148
|
post_install_message:
|
@@ -156,7 +157,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
156
157
|
version: '0'
|
157
158
|
segments:
|
158
159
|
- 0
|
159
|
-
hash:
|
160
|
+
hash: 3909050601515988109
|
160
161
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
162
|
none: false
|
162
163
|
requirements:
|
@@ -165,17 +166,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
166
|
version: '0'
|
166
167
|
segments:
|
167
168
|
- 0
|
168
|
-
hash:
|
169
|
+
hash: 3909050601515988109
|
169
170
|
requirements: []
|
170
171
|
rubyforge_project:
|
171
|
-
rubygems_version: 1.8.
|
172
|
+
rubygems_version: 1.8.23
|
172
173
|
signing_key:
|
173
174
|
specification_version: 3
|
174
175
|
summary: CouchRest::Changes - Observe a couch database for changes and react upon
|
175
176
|
them
|
176
177
|
test_files:
|
177
|
-
- test/
|
178
|
-
- test/setup_couch.sh
|
178
|
+
- test/setup_couchdb.rb
|
179
179
|
- test/config.yaml
|
180
|
-
- test/support/integration_test.rb
|
181
180
|
- test/test_helper.rb
|
181
|
+
- test/support/integration_helper.rb
|
182
|
+
- test/test.netrc
|
183
|
+
- test/integration/callback_test.rb
|
data/Readme.md
DELETED
@@ -1,75 +0,0 @@
|
|
1
|
-
CouchRest::Changes - keeping track of changes to your couch
|
2
|
-
------------------------------------------------------------
|
3
|
-
|
4
|
-
``CouchRest::Changes`` let's you observe a couch database for changes and react upon them.
|
5
|
-
|
6
|
-
Following the changes of a couch is as easy as
|
7
|
-
```ruby
|
8
|
-
users = CouchRest::Changes.new('users')
|
9
|
-
```
|
10
|
-
|
11
|
-
Callbacks can be defined in blocks:
|
12
|
-
```ruby
|
13
|
-
users.created do |hash|
|
14
|
-
puts "A new user was created with the id: #{hash[:id]}"
|
15
|
-
end
|
16
|
-
```
|
17
|
-
|
18
|
-
To start listening just call
|
19
|
-
```ruby
|
20
|
-
users.listen
|
21
|
-
```
|
22
|
-
|
23
|
-
This program is written in Ruby and is distributed under the following license:
|
24
|
-
|
25
|
-
> GNU Affero General Public License
|
26
|
-
> Version 3.0 or higher
|
27
|
-
> http://www.gnu.org/licenses/agpl-3.0.html
|
28
|
-
|
29
|
-
Installation
|
30
|
-
---------------------
|
31
|
-
|
32
|
-
Just add couchrest_changes to your gemfile.
|
33
|
-
|
34
|
-
Configuration
|
35
|
-
---------------------
|
36
|
-
|
37
|
-
``couchrest_changes`` can be configured through ``CouchRest::Changes::Config``
|
38
|
-
|
39
|
-
The default options are similar to the ones used by CouchRest::Model:
|
40
|
-
|
41
|
-
|
42
|
-
```yaml
|
43
|
-
# couch connection configuration
|
44
|
-
connection:
|
45
|
-
protocol: "http"
|
46
|
-
host: "localhost"
|
47
|
-
port: 5984
|
48
|
-
username: ~
|
49
|
-
password: ~
|
50
|
-
prefix: ""
|
51
|
-
suffix: ""
|
52
|
-
|
53
|
-
# file to store the last processed user record in so we can resume after
|
54
|
-
# a restart:
|
55
|
-
seq_file: "/var/log/couch_changes_users.seq"
|
56
|
-
|
57
|
-
# Configure log_file like this if you want to log to a file instead of syslog:
|
58
|
-
# log_file: "/var/log/couch_changes.log"
|
59
|
-
log_level: debug
|
60
|
-
|
61
|
-
options:
|
62
|
-
your_own_options: "go here"
|
63
|
-
```
|
64
|
-
|
65
|
-
Examples
|
66
|
-
------------------------
|
67
|
-
|
68
|
-
See [tapicero](https://github.com/leapcode/tapicero) for a daemon that uses CouchRest::Changes. Historically CouchRest::Changes was extracted from tapicero.
|
69
|
-
|
70
|
-
Known Issues
|
71
|
-
-------------
|
72
|
-
|
73
|
-
* CouchRest will miss the first change in a continuous feed.
|
74
|
-
https://github.com/couchrest/couchrest/pull/104 has a fix.
|
75
|
-
You might want to monkeypatch it.
|
data/test/setup_couch.sh
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
#!/bin/bash
|
2
|
-
|
3
|
-
HOST="http://localhost:5984"
|
4
|
-
echo "couch version :"
|
5
|
-
curl -X GET $HOST
|
6
|
-
echo "creating unprivileged user :"
|
7
|
-
curl -HContent-Type:application/json -XPUT $HOST/_users/org.couchdb.user:me --data-binary '{"_id": "org.couchdb.user:me","name": "me","roles": [],"type": "user","password": "pwd"}'
|
8
|
-
echo "creating database to watch:"
|
9
|
-
curl -X PUT $HOST/couchrest_changes_test_records
|
10
|
-
echo "restricting database access :"
|
11
|
-
curl -X PUT $HOST/couchrest_changes_test_records/_security -Hcontent-type:application/json --data-binary '{"admins":{"names":[],"roles":[]},"members":{"names":["me"],"roles":[]}}'
|
12
|
-
echo "adding admin :"
|
13
|
-
curl -X PUT $HOST/_config/admins/anna -d '"secret"'
|
@@ -1,39 +0,0 @@
|
|
1
|
-
module CouchRest::Changes
|
2
|
-
class IntegrationTest < MiniTest::Unit::TestCase
|
3
|
-
|
4
|
-
attr_reader :config
|
5
|
-
|
6
|
-
def setup
|
7
|
-
@config ||= CouchRest::Changes::Config.load BASE_DIR,
|
8
|
-
'test/config.yaml'
|
9
|
-
end
|
10
|
-
|
11
|
-
def create(fast = false)
|
12
|
-
result = database.save_doc :some => :content
|
13
|
-
raise RuntimeError.new(result.inspect) unless result['ok']
|
14
|
-
@record = {'_id' => result["id"], '_rev' => result["rev"]}
|
15
|
-
sleep 1
|
16
|
-
end
|
17
|
-
|
18
|
-
def delete(fast = false)
|
19
|
-
return if @record.nil? or @record['_deleted']
|
20
|
-
result = database.delete_doc @record
|
21
|
-
raise RuntimeError.new(result.inspect) unless result['ok']
|
22
|
-
@record['_deleted'] = true
|
23
|
-
sleep 1
|
24
|
-
end
|
25
|
-
|
26
|
-
def database
|
27
|
-
@database ||= host.database(database_name)
|
28
|
-
end
|
29
|
-
|
30
|
-
def database_name
|
31
|
-
config.complete_db_name('records')
|
32
|
-
end
|
33
|
-
|
34
|
-
def host
|
35
|
-
@host ||= CouchRest.new(config.couch_host)
|
36
|
-
end
|
37
|
-
|
38
|
-
end
|
39
|
-
end
|