ns_connector 0.0.6

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.
Files changed (42) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +80 -0
  3. data/Guardfile +9 -0
  4. data/HACKING +31 -0
  5. data/LICENSE.txt +7 -0
  6. data/README.rdoc +191 -0
  7. data/Rakefile +45 -0
  8. data/VERSION +1 -0
  9. data/lib/ns_connector.rb +4 -0
  10. data/lib/ns_connector/attaching.rb +42 -0
  11. data/lib/ns_connector/chunked_searching.rb +111 -0
  12. data/lib/ns_connector/config.rb +66 -0
  13. data/lib/ns_connector/errors.rb +79 -0
  14. data/lib/ns_connector/field_store.rb +19 -0
  15. data/lib/ns_connector/hash.rb +11 -0
  16. data/lib/ns_connector/resource.rb +288 -0
  17. data/lib/ns_connector/resources.rb +3 -0
  18. data/lib/ns_connector/resources/contact.rb +279 -0
  19. data/lib/ns_connector/resources/customer.rb +355 -0
  20. data/lib/ns_connector/resources/invoice.rb +466 -0
  21. data/lib/ns_connector/restlet.rb +137 -0
  22. data/lib/ns_connector/sublist.rb +21 -0
  23. data/lib/ns_connector/sublist_item.rb +25 -0
  24. data/misc/failed_sublist_saving_patch +547 -0
  25. data/scripts/run_restlet +25 -0
  26. data/scripts/test_shell +21 -0
  27. data/spec/attaching_spec.rb +48 -0
  28. data/spec/chunked_searching_spec.rb +75 -0
  29. data/spec/config_spec.rb +43 -0
  30. data/spec/resource_spec.rb +340 -0
  31. data/spec/resources/contact_spec.rb +8 -0
  32. data/spec/resources/customer_spec.rb +8 -0
  33. data/spec/resources/invoice_spec.rb +20 -0
  34. data/spec/restlet_spec.rb +135 -0
  35. data/spec/spec_helper.rb +16 -0
  36. data/spec/sublist_item_spec.rb +25 -0
  37. data/spec/sublist_spec.rb +45 -0
  38. data/spec/support/mock_data.rb +10 -0
  39. data/support/read_only_test +63 -0
  40. data/support/restlet.js +384 -0
  41. data/support/super_dangerous_write_test +85 -0
  42. metadata +221 -0
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(
3
+ File.dirname(__FILE__),
4
+ '..', 'lib'
5
+ )
6
+
7
+ require 'ns_connector'
8
+ require 'pp'
9
+
10
+ unless ARGV.size == 2 then
11
+ warn(
12
+ "Usage: #{$0}"\
13
+ "<configuration as ruby code> "\
14
+ "<arguments as ruby code>"
15
+ )
16
+ warn('e.g.')
17
+ warn(
18
+ "#{$0}"\
19
+ '"{:account_id => ...}" "{:action => ...}"'
20
+ )
21
+ exit 1
22
+ end
23
+
24
+ NSConnector::Config.set_config!(eval(ARGV[0]))
25
+ pp NSConnector::Restlet.execute!(eval(ARGV[1]))
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(
3
+ File.dirname(__FILE__),
4
+ '..', 'lib'
5
+ )
6
+
7
+ require 'ns_connector'
8
+ require 'pry'
9
+
10
+ if ARGV.empty? then
11
+ warn(
12
+ "Usage: #{$0}"\
13
+ "<configuration as ruby code>"
14
+ )
15
+ exit 1
16
+ end
17
+
18
+
19
+
20
+ NSConnector::Config.set_config!(eval(ARGV[0]))
21
+ NSConnector.pry
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ class MagicResource
4
+ extend NSConnector::Attaching
5
+ def initialize upstream_store
6
+ end
7
+ def self.type_id
8
+ 'magic'
9
+ end
10
+ end
11
+
12
+ class MagicTarget
13
+ extend NSConnector::Attaching
14
+ def initialize upstream_store
15
+ end
16
+ def self.type_id
17
+ 'target'
18
+ end
19
+ end
20
+
21
+ describe NSConnector::Attaching do
22
+ it 'attach! works' do
23
+ NSConnector::Restlet.should_receive(:execute!).with({
24
+ :action => 'attach',
25
+ :type_id => 'magic',
26
+ :target_type_id => 'target',
27
+ :attachee_id => 42,
28
+ :data => [1,2,3],
29
+ :attributes => nil,
30
+ })
31
+
32
+ MagicResource.attach!(MagicTarget, 42, [1,2,3])
33
+ end
34
+
35
+
36
+ it 'detach! works' do
37
+ NSConnector::Restlet.should_receive(:execute!).with({
38
+ :action => 'detach',
39
+ :type_id => 'magic',
40
+ :target_type_id => 'target',
41
+ :attachee_id => 42,
42
+ :data => [1,2,3]
43
+ })
44
+
45
+ MagicResource.detach!(MagicTarget, 42, [1,2,3])
46
+ end
47
+ end
48
+
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ class MagicResource
4
+ extend NSConnector::ChunkedSearching
5
+ def initialize upstream_store
6
+ end
7
+ def self.type_id
8
+ 'magic'
9
+ end
10
+ end
11
+
12
+ describe NSConnector::ChunkedSearching do
13
+ before :all do
14
+ # Keep things exciting
15
+ NSConnector::Config[:no_threads] = rand(15) + 1
16
+ end
17
+
18
+ def mock_chunked_search
19
+ (0..5).each do |i|
20
+ MagicResource.should_receive(:grab_chunk).
21
+ with('filters', i).ordered.and_return([1,2,3])
22
+ end
23
+
24
+ MagicResource.should_receive(:grab_chunk).
25
+ with('filters', 6).ordered.
26
+ and_raise(NSConnector::Errors::EndChunking, double)
27
+ end
28
+
29
+ it 'grabs a single chunk' do
30
+ NSConnector::Restlet.should_receive(:execute!).
31
+ with({
32
+ :action => 'search',
33
+ :type_id => 'magic',
34
+ :data => {
35
+ :filters => 'filters',
36
+ :chunk => 42
37
+ }
38
+ }).and_return(['retval'])
39
+
40
+ MagicResource.should_receive(:new).with('retval')
41
+ expect(MagicResource.grab_chunk('filters', 42)).to be_a(Array)
42
+ end
43
+
44
+ it 'listens to the config' do
45
+ MagicResource.should_receive(:threaded_search_by_chunks).
46
+ with('filters').ordered.
47
+ and_return(1)
48
+ MagicResource.should_receive(:normal_search_by_chunks).
49
+ with('filters').ordered.
50
+ and_return(2)
51
+
52
+ expect(MagicResource.search_by_chunks('filters')).to eql(1)
53
+ NSConnector::Config[:use_threads] = false
54
+ expect(MagicResource.search_by_chunks('filters')).to eql(2)
55
+ end
56
+
57
+ it 'does a normal search correctly' do
58
+ mock_chunked_search
59
+
60
+ ret = MagicResource.normal_search_by_chunks('filters')
61
+
62
+ expect(ret).to be_a(Array)
63
+ # 5 iterations returning 3 each
64
+ expect(ret).to have(6 * 3).things
65
+ end
66
+
67
+ it 'does a threaded search correctly' do
68
+ mock_chunked_search
69
+
70
+ ret = MagicResource.threaded_search_by_chunks('filters')
71
+ expect(ret).to be_a(Array)
72
+ expect(ret).to have(6 * 3).things
73
+ end
74
+
75
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ describe NSConnector::Config do
3
+ before(:each) do
4
+ # Reset our 'global' config
5
+ # Yes, it's kinda not nice to have a global configuration like
6
+ # this, but it's better than passing the damned thing around
7
+ # everywhere. Thread safety should be fine for reads.
8
+ NSConnector::Config.set_config!({})
9
+ end
10
+
11
+ it 'sets a valid config' do
12
+ NSConnector::Config.set_config!(valid_config)
13
+ expect(NSConnector::Config.check_valid!).to be_true
14
+ end
15
+
16
+ it 'sets an invalid config' do
17
+ NSConnector::Config.set_config!(:invalid => true)
18
+ expect{NSConnector::Config.check_valid!}.to raise_error
19
+ end
20
+
21
+ it 'allows reading of keys' do
22
+ expect(NSConnector::Config[:account_id]).to be_nil
23
+
24
+ NSConnector::Config.set_config!(valid_config)
25
+
26
+ expect(NSConnector::Config[:account_id]).to eql('account_id')
27
+ expect(NSConnector::Config['account_id']).to eql('account_id')
28
+ end
29
+
30
+ it 'allows writing of keys' do
31
+ expect(NSConnector::Config[:account_id]).to be_nil
32
+ NSConnector::Config[:account_id] = 'account_id'
33
+ expect(NSConnector::Config[:account_id]).to eql('account_id')
34
+
35
+ NSConnector::Config['account_id'] = nil
36
+ expect(NSConnector::Config[:account_id]).to be_nil
37
+ end
38
+
39
+ it 'has defaults' do
40
+ expect(NSConnector::Config['use_threads']).
41
+ to eql(!!NSConnector::Config['use_threads'])
42
+ end
43
+ end
@@ -0,0 +1,340 @@
1
+ require 'spec_helper'
2
+ include NSConnector
3
+
4
+ class PseudoResource < Resource
5
+ # The NetSuite internal id for the object. For a Contact, it would be
6
+ # 'contact'
7
+ @type_id = 'pseudoresource'
8
+ @fields = ['id', 'firstname', 'lastname']
9
+ @sublists = {:notes => [:line]}
10
+ end
11
+
12
+ # We create another resource here to ensure no clashes in anything shared.
13
+ class OtherResource < Resource
14
+ @type_id = 'otherresource'
15
+ @fields = ['id', 'fax']
16
+ @sublists = {}
17
+ end
18
+
19
+
20
+ describe PseudoResource do
21
+ before :each do
22
+ @p = PseudoResource.new
23
+ end
24
+
25
+ context '#new' do
26
+ it 'has things we expect' do
27
+ PseudoResource.type_id.should eql('pseudoresource')
28
+ PseudoResource.fields.should eql(
29
+ ['id', 'firstname', 'lastname']
30
+ )
31
+ PseudoResource.sublists.should eql({:notes => [:line]})
32
+ end
33
+
34
+ it 'automatically loads field ids just once on #new' do
35
+ PseudoResource.new
36
+ expect(@p.fields).
37
+ to eql(['id', 'firstname', 'lastname'])
38
+
39
+ OtherResource.new
40
+ o = OtherResource.new
41
+ expect(o.fields).to eql(['id', 'fax'])
42
+ end
43
+
44
+ it 'allows us to pass stuff to new' do
45
+ new_resource = PseudoResource.new(:firstname => 'first')
46
+ expect(new_resource.firstname).to eql('first')
47
+ expect(new_resource.lastname).to be_nil
48
+ expect(new_resource).to_not be_in_netsuite
49
+
50
+ in_ns_resource = PseudoResource.new(
51
+ {:lastname => 'last'}, true
52
+ )
53
+ expect(in_ns_resource.lastname).to eql('last')
54
+ expect(in_ns_resource.firstname).to be_nil
55
+ expect(in_ns_resource).to be_in_netsuite
56
+ end
57
+
58
+ it 'allows fields to be read and set' do
59
+ expect(@p.id).to be_nil
60
+ expect(@p.firstname).to be_nil
61
+ expect(@p.lastname).to be_nil
62
+
63
+ @p.firstname = 'first'
64
+ expect(@p.firstname).to eql('first')
65
+ expect(@p.store).to eql({'firstname' => 'first'})
66
+
67
+ # Ensure there is no odd behaviour here
68
+ p2 = PseudoResource.new
69
+ p2.firstname = 'second'
70
+
71
+ expect(@p.firstname).to eql('first')
72
+ expect(p2.firstname).to eql('second')
73
+ end
74
+
75
+ it 'does not exist in netsuite as it is new' do
76
+ expect(PseudoResource.new).to_not be_in_netsuite
77
+ end
78
+
79
+ it 'saves as a new object when #save! is called' do
80
+ ns_reply = {
81
+ 'firstname' => 'Name',
82
+ 'lastname' => 'nothing',
83
+ 'id' => '42'
84
+ }
85
+
86
+ @p.firstname = 'name'
87
+
88
+ Restlet.should_receive(:execute!).
89
+ with({
90
+ :action => 'create',
91
+ :type_id => 'pseudoresource',
92
+ :fields => ['id', 'firstname', 'lastname'],
93
+ :data => {'firstname' => 'name'}
94
+ }).
95
+ once.
96
+ and_return(ns_reply)
97
+
98
+ expect(@p.save!).to eql(true)
99
+
100
+ expect(@p.firstname).to eql('Name')
101
+ expect(@p.lastname).to eql('nothing')
102
+ expect(@p.id).to eql('42')
103
+ end
104
+
105
+ it 'has a pretty inspect' do
106
+ expect(@p.inspect).to eql(
107
+ '#<NSConnector::PseudoResource:nil>'
108
+ )
109
+
110
+ @p.instance_variable_set('@store', {'id' => 1})
111
+ expect(@p.inspect).to eql(
112
+ '#<NSConnector::PseudoResource:1>'
113
+ )
114
+ end
115
+ end
116
+
117
+ context 'reading' do
118
+ it 'retrieves one resource with #find' do
119
+ Restlet.should_receive(:execute!).
120
+ with({
121
+ :action => 'retrieve',
122
+ :type_id => 'pseudoresource',
123
+ :fields => [
124
+ 'id',
125
+ 'firstname',
126
+ 'lastname'
127
+ ],
128
+ :data => {'id' => 42}
129
+ }).and_return({'firstname' => 'dude man'})
130
+
131
+ result = PseudoResource.find(42)
132
+ expect(result).to be_a(PseudoResource)
133
+ expect(result.firstname).to eql('dude man')
134
+ expect(result).to be_in_netsuite
135
+ end
136
+
137
+ it 'retrieves multiple resources with #advanced_search' do
138
+ expected_filters = [
139
+ ['entityId', nil, 'is', '42'],
140
+ ['email', nil, 'isempty']
141
+ ]
142
+
143
+ Restlet.should_receive(:execute!).
144
+ with({
145
+ :action => 'search',
146
+ :type_id => 'pseudoresource',
147
+ :fields => [
148
+ 'id',
149
+ 'firstname',
150
+ 'lastname'
151
+ ],
152
+ :data => {:filters => expected_filters}
153
+ }).and_return([
154
+ {'id' => 1, 'firstname' => 'unique'},
155
+ {'id' => 2, 'firstname' => 'two'}
156
+ ])
157
+
158
+ result = PseudoResource.advanced_search([
159
+ ['entityId', nil, 'is', '42'],
160
+ ['email', nil, 'isempty']
161
+ ])
162
+ expect(result).to be_a(Array)
163
+ expect(result).to have(2).things
164
+
165
+ expect(result.first).to be_a(PseudoResource)
166
+ expect(result.first.firstname).to eql('unique')
167
+ expect(result.first.id).to eql(1)
168
+ expect(result.first).to be_in_netsuite
169
+
170
+ expect(result.last.firstname).to eql('two')
171
+ end
172
+
173
+ it 'has convenience method #search_by' do
174
+ PseudoResource.
175
+ should_receive(:advanced_search).
176
+ with([['a', nil, 'is', 'b']]).
177
+ and_return('nothing')
178
+ expect(PseudoResource.search_by('a', 'b')).
179
+ to eql('nothing')
180
+ end
181
+
182
+ it 'has convenience method #all' do
183
+ PseudoResource.
184
+ should_receive(:advanced_search).
185
+ with([]).
186
+ and_return('nothing')
187
+ expect(PseudoResource.all).to eql('nothing')
188
+ end
189
+
190
+ it 'tries to chunk given BeginChunking' do
191
+ Restlet.should_receive(:execute!).
192
+ and_raise(Errors::BeginChunking, double)
193
+ PseudoResource.should_receive(:search_by_chunks).
194
+ and_return('hai')
195
+ expect(PseudoResource.all).to eql('hai')
196
+ end
197
+ end
198
+
199
+ shared_context 'found_resource' do
200
+ before :each do
201
+ Restlet.should_receive(:execute!).
202
+ with({
203
+ :action => 'retrieve',
204
+ :type_id => 'pseudoresource',
205
+ :fields => [
206
+ 'id',
207
+ 'firstname',
208
+ 'lastname'
209
+ ],
210
+ :data => {'id' => 42}
211
+ }).and_return(
212
+ {
213
+ 'id' => '1',
214
+ 'firstname' => 'orig',
215
+ 'lastname' => 'orig'
216
+ }
217
+ )
218
+ @found_resource = PseudoResource.find(42)
219
+ expect(@found_resource.firstname).to eql('orig')
220
+ expect(@found_resource).to be_in_netsuite
221
+ end
222
+ end
223
+
224
+ context 'updating' do
225
+ include_context 'found_resource'
226
+
227
+ it 'updates existing record in NetSuite' do
228
+ @found_resource.firstname = 'new'
229
+ expect(@found_resource.firstname).to eql('new')
230
+
231
+ Restlet.should_receive(:execute!).
232
+ with({
233
+ :action => 'update',
234
+ :type_id => 'pseudoresource',
235
+ :fields => [
236
+ 'id',
237
+ 'firstname',
238
+ 'lastname'
239
+ ],
240
+ :data => {
241
+ 'firstname' => 'new',
242
+ 'lastname' => 'orig',
243
+ 'id' => '1'
244
+ }
245
+ }).
246
+ once.
247
+ and_return({'firstname' => 'New'})
248
+
249
+ @found_resource.save!
250
+
251
+ expect(@found_resource.lastname).to be_nil
252
+ expect(@found_resource.firstname).to eql('New')
253
+ end
254
+ end
255
+
256
+ context 'deleting' do
257
+ include_context 'found_resource'
258
+
259
+ it 'tries to delete ID via class method' do
260
+ Restlet.should_receive(:execute!).
261
+ with({
262
+ :action => 'delete',
263
+ :type_id => 'pseudoresource',
264
+ :data => {
265
+ 'id' => 42
266
+ }
267
+ }).
268
+ once.and_return([])
269
+ PseudoResource.delete!(42)
270
+ end
271
+
272
+ it 'tries to delete ID via instance method' do
273
+ Restlet.should_receive(:execute!).
274
+ with({
275
+ :action => 'delete',
276
+ :type_id => 'pseudoresource',
277
+ :data => {
278
+ 'id' => 1
279
+ }
280
+ }).
281
+ once.and_return([])
282
+ @found_resource.delete!
283
+ end
284
+
285
+ it 'returns false trying to delete an object not in NS' do
286
+ expect(@p.delete!).to be_false
287
+ end
288
+ end
289
+
290
+ context 'sublists on new object' do
291
+ it 'is empty' do
292
+ p = PseudoResource.new
293
+ expect(p.notes).to be_empty
294
+ end
295
+ end
296
+
297
+ context 'raw search' do
298
+ it 'has #raw_search' do
299
+ Restlet.should_receive(:execute!).
300
+ with({
301
+ :action => 'raw_search',
302
+ :type_id => 'pseudoresource',
303
+ :fields => [
304
+ 'id', 'firstname', 'lastname'
305
+ ],
306
+ :data => {
307
+ :columns => [['a']],
308
+ :filters => [['b']]
309
+ }
310
+ }).
311
+ once.and_return(['hai'])
312
+ expect(PseudoResource.raw_search([['a']], [['b']])).
313
+ to eql(['hai'])
314
+ end
315
+ end
316
+
317
+ context 'link aliases' do
318
+ include_context 'found_resource'
319
+
320
+ it 'has #link' do
321
+ PseudoResource.should_receive(:attach!).
322
+ with(OtherResource, '1', [1,2], nil)
323
+ @found_resource.attach!(OtherResource, [1,2])
324
+
325
+ expect{@p.attach!(OtherResource, [1,2])}.to raise_error(
326
+ ::ArgumentError, /need an id/i
327
+ )
328
+ end
329
+
330
+ it 'has #unlink' do
331
+ PseudoResource.should_receive(:detach!).
332
+ with(OtherResource, '1', [1,2])
333
+ @found_resource.detach!(OtherResource, [1,2])
334
+
335
+ expect{@p.detach!(OtherResource, [1,2])}.to raise_error(
336
+ ::ArgumentError, /need an id/i
337
+ )
338
+ end
339
+ end
340
+ end