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