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,137 @@
1
+ require 'net/https'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'ns_connector/config'
5
+ require 'ns_connector/errors'
6
+
7
+ # Connects to the RESTlet configured in NetSuite to make generic requests.
8
+ # Example usage:
9
+ # NSConnector::Restlet.execute!(
10
+ # :action => 'retrieve',
11
+ # :type_id => type_id,
12
+ # :data => {'_id' => Integer(id)}
13
+ # )
14
+ # => parsed json results
15
+ module NSConnector::Restlet
16
+ RuntimeError = Class.new(Exception)
17
+ ArgumentError = Class.new(Exception)
18
+
19
+ # Build a HTTP request to connect to NetSuite RESTlets, then request
20
+ # our magic restlet that can do everything useful.
21
+ #
22
+ # Returns:: The parsed JSON response, i.e. JSON.parse(response.body)
23
+ def self.execute! options
24
+ NSConnector::Config.check_valid!
25
+
26
+ # Build our request up, a bit ugly
27
+ uri = URI(NSConnector::Config[:restlet_url])
28
+
29
+ unless uri.scheme then
30
+ raise NSConnector::Restlet::ArgumentError,
31
+ 'Configuration value restlet_url must at '\
32
+ 'least contain a scheme (i.e. http://)'
33
+ end
34
+
35
+ http = Net::HTTP.new(uri.host, uri.port)
36
+
37
+ if NSConnector::Config[:debug] then
38
+ http.set_debug_output $stderr
39
+ end
40
+
41
+ http.use_ssl = (uri.scheme == 'https')
42
+
43
+ request = Net::HTTP::Post.new("#{uri.path}?#{uri.query}")
44
+
45
+ request['Content-Type'] = 'application/json'
46
+ request['Authorization'] = NSConnector::Restlet.auth_header
47
+
48
+ begin
49
+ options[:code] = restlet_code
50
+ request.body = JSON.dump(options)
51
+ rescue JSON::GeneratorError => current
52
+ exception = NSConnector::Restlet::ArgumentError.new(
53
+ "Failed to convert options (#{options}) " \
54
+ "into JSON: #{current.message}"
55
+ )
56
+
57
+ exception.set_backtrace(current.backtrace)
58
+ raise exception
59
+ end
60
+
61
+ response = http.request(request)
62
+
63
+ # Netsuite seems to use HTTP 400 (bad requests) for all runtime
64
+ # errors. Hah.
65
+ if response.kind_of? Net::HTTPBadRequest then
66
+ # So let's try and raise this exception as something
67
+ # nicer.
68
+ NSConnector::Errors.try_handle_response!(response)
69
+ end
70
+
71
+ unless response.kind_of? Net::HTTPSuccess then
72
+ raise NSConnector::Restlet::RuntimeError.new(
73
+ 'Restlet execution failed, expected a '\
74
+ 'HTTP 2xx response, got a HTTP '\
75
+ "#{response.code}: #{response.body}"
76
+ )
77
+ end
78
+
79
+ if response.body.nil? then
80
+ raise NSConnector::Restlet::RuntimeError.new(
81
+ "Recieved a blank response from RESTlet"
82
+ )
83
+ end
84
+
85
+ begin
86
+ return JSON.parse(response.body)
87
+ rescue JSON::ParserError => current
88
+ exception = NSConnector::Restlet::RuntimeError.new(
89
+ 'Failed to parse response ' \
90
+ 'from Restlet as JSON '\
91
+ "(#{response.body}): " \
92
+ "#{current.message}"
93
+ )
94
+ exception.set_backtrace(current.backtrace)
95
+ raise exception
96
+ end
97
+ end
98
+
99
+ # Generate a NetSuite specific Authorization header.
100
+ #
101
+ # From the NetSuite documentation:
102
+ #
103
+ # NLAuth passes in the following login credentials:
104
+ # +nlauth_account+:: NetSuite company ID (required)
105
+ # +nlauth_email+:: NetSuite user name (required)
106
+ # +nclauth_signature+:: NetSuite password (required)
107
+ # +nlauth_role+:: internal ID of the role used to log in to
108
+ # NetSuite (optional)
109
+ # The Authorization header should be formatted as:
110
+ # NLAuth<space><comma-separated parameters>
111
+ #
112
+ # For example:
113
+ # Authorization: NLAuth nlauth_account=123456, \
114
+ # nlauth_email=jsmith@ABC.com, nlauth_signature=xxxxxxxx, \
115
+ # nlauth_role=41
116
+ #
117
+ # Returns:: String with the contents of the header
118
+ def self.auth_header
119
+ c = NSConnector::Config
120
+ c.check_valid!
121
+
122
+ "NLAuth nlauth_account=#{c[:account_id]}," \
123
+ "nlauth_email=#{c[:email]}," \
124
+ "nlauth_role=#{c[:role]}," \
125
+ "nlauth_signature=#{URI.escape(c[:password])}"
126
+ end
127
+
128
+ # Retrieve restlet code from support/restlet.js
129
+ # Returns:: String
130
+ def self.restlet_code
131
+ restlet_location = File.join(
132
+ File.dirname(__FILE__),
133
+ '..', '..', 'support', 'restlet.js'
134
+ )
135
+ File.read(restlet_location)
136
+ end
137
+ end
@@ -0,0 +1,21 @@
1
+ # Provides a method for grabbing sublists
2
+ module NSConnector::SubList
3
+ # Grab sublist_id from NetSuite
4
+ # Returns:: An array of SubListItems
5
+ def self.fetch parent, sublist_id, fields
6
+ NSConnector::Restlet.execute!(
7
+ :action => 'fetch_sublist',
8
+ :type_id => parent.type_id,
9
+ :parent_id => parent.id,
10
+ :fields => fields,
11
+ :sublist_id => sublist_id
12
+ ).map do |upstream_store|
13
+ NSConnector::SubListItem.new(
14
+ sublist_id,
15
+ fields,
16
+ parent,
17
+ upstream_store
18
+ )
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ require 'ns_connector/field_store'
2
+ require 'ns_connector/hash'
3
+
4
+ # Represents one SubListItem under a Resource
5
+ class NSConnector::SubListItem
6
+ include NSConnector::FieldStore
7
+ attr_accessor :store
8
+ attr_accessor :parent
9
+ attr_accessor :name
10
+ attr_accessor :fields
11
+
12
+ def initialize name, fields, parent, upstream_store = nil
13
+ upstream_store.stringify_keys! if upstream_store
14
+ @store = (upstream_store || {})
15
+ @parent = parent
16
+ @fields = fields
17
+ @name = name
18
+
19
+ create_store_accessors!
20
+ end
21
+
22
+ def inspect
23
+ "#<NSConnector::#{self.class}:#{name}>"
24
+ end
25
+ end
@@ -0,0 +1,547 @@
1
+ Why is this stored here and not in git?
2
+
3
+ I'm planning rebase into a single commit, prefer not to lose this as a reference.
4
+
5
+ diff --git a/lib/ns_connector/resource.rb b/lib/ns_connector/resource.rb
6
+ index 92bbeab..3e49d7b 100644
7
+ --- a/lib/ns_connector/resource.rb
8
+ +++ b/lib/ns_connector/resource.rb
9
+ @@ -94,17 +94,6 @@ class NSConnector::Resource
10
+ # If we got this far, we're definitely in NetSuite
11
+ @in_netsuite = true
12
+
13
+ - # Now we save our sublist(s)
14
+ - @sublist_store.each do |sublist_id, sublist_items|
15
+ - # Overwriting the current item
16
+ - @sublist_store[sublist_id] = NSConnector::SubList.save!(
17
+ - sublist_items,
18
+ - self,
19
+ - sublist_id,
20
+ - sublists[sublist_id]
21
+ - )
22
+ - end
23
+ -
24
+ return true
25
+ end
26
+
27
+ @@ -228,12 +217,7 @@ class NSConnector::Resource
28
+ private
29
+ # Given a sublist of {:addressbook => ['fields']} we want a method
30
+ # addressbook that looks up the sublist if we have an ID, otherwise
31
+ - # returns the empty array.
32
+ - #
33
+ - # And finally we need a method to create new sublist objects that is
34
+ - # generic and not too crazy. So we have new_addressbook that returns a
35
+ - # SubList object that can be stored and later turned into a hash to
36
+ - # send to NetSuite.
37
+ + # returns an empty array.
38
+ def create_sublist_accessors!
39
+ sublists.each do |sublist_name, fields|
40
+ self.class.class_eval do
41
+ @@ -250,21 +234,6 @@ class NSConnector::Resource
42
+
43
+ @sublist_store[sublist_name] ||= []
44
+ end
45
+ -
46
+ - define_method("#{sublist_name}=") do |value|
47
+ - @sublist_store[sublist_name] = value
48
+ - end
49
+ -
50
+ - define_method(
51
+ - "new_#{sublist_name}_item"
52
+ - ) do |upstream_store = nil|
53
+ - NSConnector::SubListItem.new(
54
+ - sublist_name,
55
+ - fields,
56
+ - self,
57
+ - upstream_store
58
+ - )
59
+ - end
60
+ end
61
+ end
62
+ end
63
+ diff --git a/lib/ns_connector/sublist.rb b/lib/ns_connector/sublist.rb
64
+ index b24f0c1..94d9c2f 100644
65
+ --- a/lib/ns_connector/sublist.rb
66
+ +++ b/lib/ns_connector/sublist.rb
67
+ @@ -1,27 +1,5 @@
68
+ # Provides a method for saving a bunch of SubListItems
69
+ module NSConnector::SubList
70
+ - # We save our array of SubListItems in the order in which they appear.
71
+ - # Arguments:: An array of SubListItem, the parent object and the fields
72
+ - # Returns:: An array of SubListItem that have been saved
73
+ - def self.save! sublist_items, parent, sublist_id, fields
74
+ - data = sublist_items.uniq.map do |item|
75
+ - item.store
76
+ - end
77
+ -
78
+ - NSConnector::Restlet.execute!(
79
+ - :action => 'save_sublist',
80
+ - :type_id => parent.type_id,
81
+ - :parent_id => parent.id,
82
+ - :fields => fields,
83
+ - :sublist_id => sublist_id,
84
+ - :data => data
85
+ - )
86
+ -
87
+ - # We have to do this in a second request as NetSuite needs a
88
+ - # short time to think about any added records.
89
+ - return NSConnector::SubList.fetch(parent, sublist_id, fields)
90
+ - end
91
+ -
92
+ # Grab sublist_id from NetSuite
93
+ # Returns:: An array of SubListItems
94
+ def self.fetch parent, sublist_id, fields
95
+ diff --git a/misc/failed_sublist_saving_patch b/misc/failed_sublist_saving_patch
96
+ index 5393f12..e69de29 100644
97
+ --- a/misc/failed_sublist_saving_patch
98
+ +++ b/misc/failed_sublist_saving_patch
99
+ @@ -1,214 +0,0 @@
100
+ -diff --git a/lib/ns_connector/resource.rb b/lib/ns_connector/resource.rb
101
+ -index 92bbeab..ef0f649 100644
102
+ ---- a/lib/ns_connector/resource.rb
103
+ -+++ b/lib/ns_connector/resource.rb
104
+ -@@ -94,17 +94,6 @@ class NSConnector::Resource
105
+ - # If we got this far, we're definitely in NetSuite
106
+ - @in_netsuite = true
107
+ -
108
+ -- # Now we save our sublist(s)
109
+ -- @sublist_store.each do |sublist_id, sublist_items|
110
+ -- # Overwriting the current item
111
+ -- @sublist_store[sublist_id] = NSConnector::SubList.save!(
112
+ -- sublist_items,
113
+ -- self,
114
+ -- sublist_id,
115
+ -- sublists[sublist_id]
116
+ -- )
117
+ -- end
118
+ --
119
+ - return true
120
+ - end
121
+ -
122
+ -@@ -250,21 +239,6 @@ class NSConnector::Resource
123
+ -
124
+ - @sublist_store[sublist_name] ||= []
125
+ - end
126
+ --
127
+ -- define_method("#{sublist_name}=") do |value|
128
+ -- @sublist_store[sublist_name] = value
129
+ -- end
130
+ --
131
+ -- define_method(
132
+ -- "new_#{sublist_name}_item"
133
+ -- ) do |upstream_store = nil|
134
+ -- NSConnector::SubListItem.new(
135
+ -- sublist_name,
136
+ -- fields,
137
+ -- self,
138
+ -- upstream_store
139
+ -- )
140
+ -- end
141
+ - end
142
+ - end
143
+ - end
144
+ -diff --git a/lib/ns_connector/sublist.rb b/lib/ns_connector/sublist.rb
145
+ -index b24f0c1..94d9c2f 100644
146
+ ---- a/lib/ns_connector/sublist.rb
147
+ -+++ b/lib/ns_connector/sublist.rb
148
+ -@@ -1,27 +1,5 @@
149
+ - # Provides a method for saving a bunch of SubListItems
150
+ - module NSConnector::SubList
151
+ -- # We save our array of SubListItems in the order in which they appear.
152
+ -- # Arguments:: An array of SubListItem, the parent object and the fields
153
+ -- # Returns:: An array of SubListItem that have been saved
154
+ -- def self.save! sublist_items, parent, sublist_id, fields
155
+ -- data = sublist_items.uniq.map do |item|
156
+ -- item.store
157
+ -- end
158
+ --
159
+ -- NSConnector::Restlet.execute!(
160
+ -- :action => 'save_sublist',
161
+ -- :type_id => parent.type_id,
162
+ -- :parent_id => parent.id,
163
+ -- :fields => fields,
164
+ -- :sublist_id => sublist_id,
165
+ -- :data => data
166
+ -- )
167
+ --
168
+ -- # We have to do this in a second request as NetSuite needs a
169
+ -- # short time to think about any added records.
170
+ -- return NSConnector::SubList.fetch(parent, sublist_id, fields)
171
+ -- end
172
+ --
173
+ - # Grab sublist_id from NetSuite
174
+ - # Returns:: An array of SubListItems
175
+ - def self.fetch parent, sublist_id, fields
176
+ -diff --git a/support/restlet.js b/support/restlet.js
177
+ -index 4569862..afc8df2 100644
178
+ ---- a/support/restlet.js
179
+ -+++ b/support/restlet.js
180
+ -@@ -193,133 +193,6 @@ function create(request)
181
+ - ));
182
+ - }
183
+ -
184
+ --// Save a sublist, trying to merge changes nicely.
185
+ --//
186
+ --// Returns:: True, note that you will need to fetch the updated items yourself,
187
+ --// as netsuite seems to need a little while to think about any newly added
188
+ --// records or it freaks out.
189
+ --//
190
+ --// Raises:: An invalid sublist operation error, often, as apparently you're not
191
+ --// allowed to delete certain sublists even though you can append to them. Kind
192
+ --// of really annoying. Not much I can do about it.
193
+ --//
194
+ --// Also seems to raise a 500 on modifying existing items if it doesn't want you
195
+ --// to.
196
+ --//
197
+ --// We have the minor problem here of only being able to modify existing line
198
+ --// items in place, insert new ones and delete old ones, not rewrite the whole
199
+ --// list. This problem is easily dealt with if we simply don't worry about
200
+ --// order. So, new items should be appended to the end, and the existing order
201
+ --// shouldn't be messed with, however if you wanted to say, pull the whole list
202
+ --// out and reverse the order, you're out of luck.
203
+ --//
204
+ --// It's possible to do, I think. I have other things to work on just now.
205
+ --//
206
+ --// So we have three basic steps:
207
+ --//
208
+ --// 1. Modify all existing records that have changed, identifying by id.
209
+ --// 2. If the existing record does not appear in the new list, delete it.
210
+ --// 3. Append new records to the end.
211
+ --function save_sublist(request)
212
+ --{
213
+ -- if(!request.hasOwnProperty('parent_id')) {
214
+ -- argument_error("Missing mandatory argument: parent_id");
215
+ -- }
216
+ --
217
+ -- if(!request.hasOwnProperty('sublist_id')) {
218
+ -- argument_error("Missing mandatory argument: sublist_id");
219
+ -- }
220
+ --
221
+ -- var record = nlapiLoadRecord(request.type_id, request.parent_id);
222
+ -- var sublist_len = record.getLineItemCount(request.sublist_id);
223
+ -- var downstream_items = request.data;
224
+ -- var we_touched_something = false;
225
+ --
226
+ -- // 1. Modify all existing records that have been changed.
227
+ -- for(var i = 1; i <= sublist_len; i++) {
228
+ -- var upstream_id = record.getLineItemValue(
229
+ -- request.sublist_id, 'id', i
230
+ -- );
231
+ -- // Search for an id within downstream items.
232
+ -- for(var j = 0; j < downstream_items.length; j++){
233
+ -- if(!downstream_items[j].hasOwnProperty('id')) {
234
+ -- // New record, not interested in this yet
235
+ -- continue;
236
+ -- }
237
+ --
238
+ -- if(downstream_items[j]['id'] == upstream_id){
239
+ -- // We have a match, we need to merge now
240
+ -- for(var field in downstream_items[j]) {
241
+ -- var upstream_value = record.getLineItemValue(
242
+ -- request.sublist_id, field, i
243
+ -- )
244
+ --
245
+ -- // Set if changed
246
+ -- if(downstream_items[j][field] != upstream_value) {
247
+ -- record.setLineItemValue(
248
+ -- request.sublist_id,
249
+ -- field,
250
+ -- i,
251
+ -- downstream_items[j][field]
252
+ -- )
253
+ -- we_touched_something = true;
254
+ -- }
255
+ -- }
256
+ -- }
257
+ -- }
258
+ -- }
259
+ --
260
+ -- // 2. Delete all existing records that aren't in our new dataset
261
+ -- for(var i = sublist_len; i > 0; i--) {
262
+ -- var match = false;
263
+ -- var upstream_id = record.getLineItemValue(
264
+ -- request.sublist_id, 'id', i
265
+ -- );
266
+ --
267
+ -- for(var j = 0; j < downstream_items.length; j++){
268
+ -- if(!downstream_items[j].hasOwnProperty('id')) {
269
+ -- continue;
270
+ -- }
271
+ -- if(downstream_items[j]['id'] == upstream_id) {
272
+ -- match = true;
273
+ -- }
274
+ -- }
275
+ --
276
+ -- // Delete this item if it wasn't in the downstream data set
277
+ -- if(!match) {
278
+ -- record.removeLineItem(request.sublist_id, i);
279
+ -- we_touched_something = true;
280
+ -- sublist_len--;
281
+ -- }
282
+ -- }
283
+ --
284
+ -- // 3. Append items
285
+ -- for(var i = 0; i < downstream_items.length; i++) {
286
+ -- if(downstream_items[i].hasOwnProperty('id')) {
287
+ -- continue;
288
+ -- }
289
+ -- // New item, append it.
290
+ -- record.insertLineItem(request.sublist_id, ++sublist_len);
291
+ -- we_touched_something = true;
292
+ --
293
+ -- for(var field in downstream_items[i]) {
294
+ -- value = downstream_items[i][field];
295
+ -- record.setLineItemValue(
296
+ -- request.sublist_id, field, i, value
297
+ -- );
298
+ -- }
299
+ -- }
300
+ --
301
+ -- // NetSuite explodes if we try to commit a sublist without altering it
302
+ -- // in any way, so we have to keep this silly flag around.
303
+ -- if(we_touched_something) {
304
+ -- record.commitLineItem(request.sublist_id);
305
+ -- nlapiSubmitRecord(record, true);
306
+ -- };
307
+ --
308
+ -- return([]);
309
+ --}
310
+ --
311
+ - // Given a record, sublist_id and array of fields, retrieve the whole sublist
312
+ - // as an array of hashes
313
+ - function get_sublist(record, sublist_id, fields)
314
+ diff --git a/spec/resource_spec.rb b/spec/resource_spec.rb
315
+ index 2d95382..7eaf835 100644
316
+ --- a/spec/resource_spec.rb
317
+ +++ b/spec/resource_spec.rb
318
+ @@ -67,16 +67,6 @@ describe PseudoResource do
319
+
320
+ @p.firstname = 'name'
321
+
322
+ - note_item1 = @p.new_notes_item(:line => 'line 1')
323
+ - note_item2 = @p.new_notes_item(:line => 'line 2')
324
+ -
325
+ - @p.notes << note_item1
326
+ - @p.notes << note_item2
327
+ -
328
+ - # Replace them, backways to test.
329
+ - SubList.should_receive(:save!).
330
+ - and_return([note_item2, note_item1])
331
+ -
332
+ Restlet.should_receive(:execute!).
333
+ with({
334
+ :action => 'create',
335
+ @@ -91,9 +81,6 @@ describe PseudoResource do
336
+
337
+ expect(@p.firstname).to eql('Name')
338
+ expect(@p.lastname).to eql('nothing')
339
+ -
340
+ - # Backwards now
341
+ - expect(@p.notes.first.line).to eql('line 2')
342
+ end
343
+
344
+ it 'has a pretty inspect' do
345
+ @@ -266,14 +253,9 @@ describe PseudoResource do
346
+ end
347
+
348
+ context 'sublists on new object' do
349
+ - it 'can append a new list to blank object' do
350
+ + it 'is empty' do
351
+ p = PseudoResource.new
352
+ - expect(p.new_notes_item).to be_a(NSConnector::SubListItem)
353
+ - p.notes << p.new_notes_item(:line => 'line 1')
354
+ - p.notes << p.new_notes_item(:line => 'line 2')
355
+ -
356
+ - expect(p.notes.pop.line).to eql('line 2')
357
+ - expect(p.notes.pop.line).to eql('line 1')
358
+ + expect(p.notes).to be_empty
359
+ end
360
+ end
361
+ end
362
+ diff --git a/spec/sublist_spec.rb b/spec/sublist_spec.rb
363
+ index 38b0147..ef2aea3 100644
364
+ --- a/spec/sublist_spec.rb
365
+ +++ b/spec/sublist_spec.rb
366
+ @@ -16,35 +16,6 @@ describe SubList do
367
+ )
368
+ end
369
+
370
+ - it 'saves' do
371
+ - Restlet.should_receive(:execute!).
372
+ - with({
373
+ - :action => 'save_sublist',
374
+ - :type_id => 'contact',
375
+ - :parent_id => '42',
376
+ - :sublist_id => 'sublist',
377
+ - :fields => ['field1', 'field2'],
378
+ - :data => [
379
+ - {'field1' => 'data'},
380
+ - {'field2' => 'otherdata'}
381
+ - ]
382
+ - }).and_return(
383
+ - 'true'
384
+ - )
385
+ -
386
+ - SubList.should_receive(:fetch).and_return('hai')
387
+ - # Duplicates should be ignored, I figure? I'll be a little
388
+ - # confused if duplicates ever appear in NetSuite
389
+ - saved = SubList.save!(
390
+ - [@item1, @item1, @item2],
391
+ - @parent,
392
+ - 'sublist',
393
+ - ['field1', 'field2']
394
+ - )
395
+ -
396
+ - expect(saved).to eql('hai')
397
+ - end
398
+ -
399
+ it 'fetches' do
400
+ Restlet.should_receive(:execute!).
401
+ with({
402
+ diff --git a/support/restlet.js b/support/restlet.js
403
+ index 4569862..3a7ad1b 100644
404
+ --- a/support/restlet.js
405
+ +++ b/support/restlet.js
406
+ @@ -193,133 +193,6 @@ function create(request)
407
+ ));
408
+ }
409
+
410
+ -// Save a sublist, trying to merge changes nicely.
411
+ -//
412
+ -// Returns:: True, note that you will need to fetch the updated items yourself,
413
+ -// as netsuite seems to need a little while to think about any newly added
414
+ -// records or it freaks out.
415
+ -//
416
+ -// Raises:: An invalid sublist operation error, often, as apparently you're not
417
+ -// allowed to delete certain sublists even though you can append to them. Kind
418
+ -// of really annoying. Not much I can do about it.
419
+ -//
420
+ -// Also seems to raise a 500 on modifying existing items if it doesn't want you
421
+ -// to.
422
+ -//
423
+ -// We have the minor problem here of only being able to modify existing line
424
+ -// items in place, insert new ones and delete old ones, not rewrite the whole
425
+ -// list. This problem is easily dealt with if we simply don't worry about
426
+ -// order. So, new items should be appended to the end, and the existing order
427
+ -// shouldn't be messed with, however if you wanted to say, pull the whole list
428
+ -// out and reverse the order, you're out of luck.
429
+ -//
430
+ -// It's possible to do, I think. I have other things to work on just now.
431
+ -//
432
+ -// So we have three basic steps:
433
+ -//
434
+ -// 1. Modify all existing records that have changed, identifying by id.
435
+ -// 2. If the existing record does not appear in the new list, delete it.
436
+ -// 3. Append new records to the end.
437
+ -function save_sublist(request)
438
+ -{
439
+ - if(!request.hasOwnProperty('parent_id')) {
440
+ - argument_error("Missing mandatory argument: parent_id");
441
+ - }
442
+ -
443
+ - if(!request.hasOwnProperty('sublist_id')) {
444
+ - argument_error("Missing mandatory argument: sublist_id");
445
+ - }
446
+ -
447
+ - var record = nlapiLoadRecord(request.type_id, request.parent_id);
448
+ - var sublist_len = record.getLineItemCount(request.sublist_id);
449
+ - var downstream_items = request.data;
450
+ - var we_touched_something = false;
451
+ -
452
+ - // 1. Modify all existing records that have been changed.
453
+ - for(var i = 1; i <= sublist_len; i++) {
454
+ - var upstream_id = record.getLineItemValue(
455
+ - request.sublist_id, 'id', i
456
+ - );
457
+ - // Search for an id within downstream items.
458
+ - for(var j = 0; j < downstream_items.length; j++){
459
+ - if(!downstream_items[j].hasOwnProperty('id')) {
460
+ - // New record, not interested in this yet
461
+ - continue;
462
+ - }
463
+ -
464
+ - if(downstream_items[j]['id'] == upstream_id){
465
+ - // We have a match, we need to merge now
466
+ - for(var field in downstream_items[j]) {
467
+ - var upstream_value = record.getLineItemValue(
468
+ - request.sublist_id, field, i
469
+ - )
470
+ -
471
+ - // Set if changed
472
+ - if(downstream_items[j][field] != upstream_value) {
473
+ - record.setLineItemValue(
474
+ - request.sublist_id,
475
+ - field,
476
+ - i,
477
+ - downstream_items[j][field]
478
+ - )
479
+ - we_touched_something = true;
480
+ - }
481
+ - }
482
+ - }
483
+ - }
484
+ - }
485
+ -
486
+ - // 2. Delete all existing records that aren't in our new dataset
487
+ - for(var i = sublist_len; i > 0; i--) {
488
+ - var match = false;
489
+ - var upstream_id = record.getLineItemValue(
490
+ - request.sublist_id, 'id', i
491
+ - );
492
+ -
493
+ - for(var j = 0; j < downstream_items.length; j++){
494
+ - if(!downstream_items[j].hasOwnProperty('id')) {
495
+ - continue;
496
+ - }
497
+ - if(downstream_items[j]['id'] == upstream_id) {
498
+ - match = true;
499
+ - }
500
+ - }
501
+ -
502
+ - // Delete this item if it wasn't in the downstream data set
503
+ - if(!match) {
504
+ - record.removeLineItem(request.sublist_id, i);
505
+ - we_touched_something = true;
506
+ - sublist_len--;
507
+ - }
508
+ - }
509
+ -
510
+ - // 3. Append items
511
+ - for(var i = 0; i < downstream_items.length; i++) {
512
+ - if(downstream_items[i].hasOwnProperty('id')) {
513
+ - continue;
514
+ - }
515
+ - // New item, append it.
516
+ - record.insertLineItem(request.sublist_id, ++sublist_len);
517
+ - we_touched_something = true;
518
+ -
519
+ - for(var field in downstream_items[i]) {
520
+ - value = downstream_items[i][field];
521
+ - record.setLineItemValue(
522
+ - request.sublist_id, field, i, value
523
+ - );
524
+ - }
525
+ - }
526
+ -
527
+ - // NetSuite explodes if we try to commit a sublist without altering it
528
+ - // in any way, so we have to keep this silly flag around.
529
+ - if(we_touched_something) {
530
+ - record.commitLineItem(request.sublist_id);
531
+ - nlapiSubmitRecord(record, true);
532
+ - };
533
+ -
534
+ - return([]);
535
+ -}
536
+ -
537
+ // Given a record, sublist_id and array of fields, retrieve the whole sublist
538
+ // as an array of hashes
539
+ function get_sublist(record, sublist_id, fields)
540
+ @@ -393,7 +266,6 @@ function main(request)
541
+ 'retrieve' : retrieve,
542
+ 'search' : search,
543
+ 'update' : update,
544
+ - 'save_sublist' : save_sublist,
545
+ 'fetch_sublist' : fetch_sublist
546
+ }
547
+