ns_connector 0.0.6

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