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,8 @@
1
+ require 'spec_helper'
2
+
3
+ include NSConnector
4
+ describe Contact do
5
+ it 'should not explode on creation' do
6
+ Contact.new
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ include NSConnector
4
+ describe Customer do
5
+ it 'should not explode on creation' do
6
+ Customer.new
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ include NSConnector
4
+ describe Invoice do
5
+ it 'should not explode on creation' do
6
+ Invoice.new
7
+ end
8
+
9
+ it 'should have a to_pdf method' do
10
+ Restlet.should_receive(:execute!).and_return(
11
+ [Base64::encode64('yay')]
12
+ )
13
+ expect{Invoice.new.to_pdf}.to raise_error(
14
+ ::ArgumentError,
15
+ /could not find id/i
16
+ )
17
+ expect(Invoice.new(:id => 1).to_pdf).to eql('yay')
18
+ end
19
+ end
20
+
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+ include NSConnector
3
+
4
+ describe Restlet do
5
+ context 'given a valid config' do
6
+ before(:each) do
7
+ NSConnector::Config.set_config! valid_config
8
+ end
9
+
10
+ it 'executes a restlet, passing the options' do
11
+ options = {:opt1 => 'value1', :opt2 => 'value2'}
12
+ expected_body = options.merge(
13
+ :code => Restlet.restlet_code
14
+ ).to_json
15
+
16
+ # We already test the auth header below. We simply test
17
+ # its existance here.
18
+ expected_request = {
19
+ :body => expected_body,
20
+ :headers => {
21
+ 'Authorization' => /^NLAuth/,
22
+ 'Content-Type'=>'application/json'
23
+ }
24
+ }
25
+
26
+ stub_request(:post, "https://netsuite:1234/restlet?").
27
+ with(expected_request).
28
+ to_return(
29
+ :status => 200,
30
+ :body => '["json"]',
31
+ )
32
+ expect(Restlet.execute!(options)).to eql(['json'])
33
+ end
34
+
35
+ it 'fails gracefully with 500' do
36
+ stub_request(:post, "https://netsuite:1234/restlet?").
37
+ to_return(:status => 500, :body => 'message')
38
+ expect{Restlet.execute!({})}.to raise_error(
39
+ Restlet::RuntimeError,
40
+ /500: message/
41
+ )
42
+ end
43
+
44
+ it 'tries to create a nice error from a 400' do
45
+ # Acutal netsuite responses as of 7 Jun 13
46
+ #
47
+ # RCRD_DSNT_EXIST -> NotFound
48
+ error = '{"error" : {"code" : "RCRD_DSNT_EXIST", '\
49
+ '"message" : "That record does not exist."}}'
50
+
51
+ stub_request(:post, "https://netsuite:1234/restlet?").
52
+ to_return(:status => 400, :body => error)
53
+ expect{Restlet.execute!({})}.to raise_error(
54
+ Errors::NotFound, /does not exist/
55
+ )
56
+
57
+ # ANYTHING represents 'CONTACT' or 'CUSTOMER', or
58
+ # Whatever
59
+ #
60
+ # ANYTHING_ALREADY_EXISTS -> Conflict
61
+ error = '{"error" : {"code" : '\
62
+ '"ANYTHING_ALREADY_EXISTS", '\
63
+ '"message" : "A contact record with this name '\
64
+ 'already exists. Every contact record must have '\
65
+ 'a unique name."}}'
66
+
67
+ stub_request(:post, "https://netsuite:1234/restlet?").
68
+ to_return(:status => 400, :body => error)
69
+ expect{Restlet.execute!({})}.to raise_error(
70
+ Errors::Conflict, /A contact record/
71
+ )
72
+ end
73
+
74
+ it 'creates a Unknown error from parseable but unknown JSON' do
75
+ error = '{"error" : {"code" : "GREEN_ROOM", '\
76
+ '"message" : "This room is... Green."}}'
77
+
78
+ stub_request(:post, "https://netsuite:1234/restlet?").
79
+ to_return(:status => 400, :body => error)
80
+ expect{Restlet.execute!({})}.to raise_error(
81
+ Errors::Unknown, /room is/
82
+ )
83
+ end
84
+
85
+ it 'creates a WTF error from unparseable JSON' do
86
+ stub_request(:post, "https://netsuite:1234/restlet?").
87
+ to_return(:status => 400, :body => 'omgwtf')
88
+ expect{Restlet.execute!({})}.to raise_error(
89
+ Errors::WTF, /omgwtf/
90
+ )
91
+ end
92
+
93
+ it 'fails gracefully with bad JSON' do
94
+ stub_request(:post, "https://netsuite:1234/restlet?").
95
+ to_return(:status => 200, :body => 'omgwtf')
96
+ expect{Restlet.execute!({})}.to raise_error(
97
+ Restlet::RuntimeError,
98
+ /omgwtf.*unexpected token/
99
+ )
100
+ end
101
+
102
+
103
+ it 'generates a valid auth header' do
104
+ expect(Restlet.auth_header).to eql(
105
+ 'NLAuth nlauth_account=account_id,' \
106
+ 'nlauth_email=email@site,' \
107
+ 'nlauth_role=123,' \
108
+ 'nlauth_signature=pass%00word'
109
+ )
110
+ end
111
+
112
+ it 'retrieves code' do
113
+ expect(Restlet.restlet_code).to be_a(String)
114
+ expect(Restlet.restlet_code).to_not be_empty
115
+ end
116
+ end
117
+
118
+ context 'given an invalid config' do
119
+ before(:each) do
120
+ NSConnector::Config.set_config!(:invalid => true)
121
+ end
122
+
123
+ it 'everything raises ArgumentError' do
124
+ expect{Restlet.execute!({})}.
125
+ to raise_error(
126
+ NSConnector::Config::ArgumentError
127
+ )
128
+
129
+ expect{Restlet.auth_header}.
130
+ to raise_error(
131
+ NSConnector::Config::ArgumentError
132
+ )
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,16 @@
1
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
2
+ $: << File.join(File.dirname(__FILE__))
3
+
4
+ require 'pry'
5
+ require 'pp'
6
+ require 'rspec'
7
+ require 'webmock/rspec'
8
+ require 'ns_connector'
9
+
10
+ require 'support/mock_data'
11
+
12
+ RSpec.configure do |config|
13
+ config.order = "random"
14
+ config.color_enabled = true
15
+ end
16
+
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ include NSConnector
3
+
4
+ describe SubListItem do
5
+ context 'new' do
6
+ before :each do
7
+ parent = double(
8
+ 'contact', :type_id => 'contact', :id => '42'
9
+ )
10
+ @s = SubListItem.new('sublist', ['field1', 'field2'], parent)
11
+ end
12
+
13
+ it 'has things we expect' do
14
+ expect(@s.field1).to eql(nil)
15
+ @s.field1 = 'yay'
16
+ expect(@s.field1).to eql('yay')
17
+ expect(@s.field2).to eql(nil)
18
+ end
19
+
20
+ it 'has a .store' do
21
+ @s.field1 = 'yay'
22
+ expect(@s.store).to eql('field1' => 'yay')
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ include NSConnector
3
+
4
+ describe SubList do
5
+ before :each do
6
+ @parent = double(
7
+ 'contact', :type_id => 'contact', :id => '42'
8
+ )
9
+ @item1 = SubListItem.new(
10
+ 'sublist', ['field1', 'field2'], @parent,
11
+ :field1 => 'data'
12
+ )
13
+ @item2 = SubListItem.new(
14
+ 'sublist', ['field1', 'field2'], @parent,
15
+ :field2 => 'otherdata'
16
+ )
17
+ end
18
+
19
+ it 'fetches' do
20
+ Restlet.should_receive(:execute!).
21
+ with({
22
+ :action => 'fetch_sublist',
23
+ :type_id => 'contact',
24
+ :parent_id => '42',
25
+ :sublist_id => 'sublist',
26
+ :fields => ['field1', 'field2'],
27
+ }).and_return(
28
+ [
29
+ {'field1' => 'new'},
30
+ {'field2' => 'new2'},
31
+ ]
32
+ )
33
+ sublist = SubList.fetch(
34
+ @parent, 'sublist', ['field1', 'field2']
35
+ )
36
+
37
+ expect(sublist).to be_a(Array)
38
+ expect(sublist).to have(2).things
39
+ sublist.each do |item|
40
+ expect(item).to be_a(SubListItem)
41
+ end
42
+ expect(sublist.first.field1).to eql('new')
43
+ expect(sublist.last.field2).to eql('new2')
44
+ end
45
+ end
@@ -0,0 +1,10 @@
1
+ # Valid configuration hash for NSConnector::Config
2
+ def valid_config
3
+ {
4
+ :account_id => 'account_id',
5
+ :email => 'email@site',
6
+ :password => "pass\0word",
7
+ :role => '123',
8
+ :restlet_url => 'https://netsuite:1234/restlet',
9
+ }
10
+ end
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(
3
+ File.dirname(__FILE__),
4
+ '..', 'lib'
5
+ )
6
+
7
+ require 'ns_connector'
8
+ require 'rspec/autorun'
9
+
10
+ unless ARGV.size == 1 then
11
+ warn "Usage: #{$0} <config as ruby code>"
12
+ warn "Warning! This script could destroy production data, it is only"\
13
+ " intended for testing"
14
+ end
15
+
16
+ # We shift the argument off here, or it gets passed to the RSpec runner
17
+ NSConnector::Config.set_config!(eval(ARGV.shift))
18
+
19
+ def customer_id
20
+ NSConnector::Config[:valid_customer_id]
21
+ end
22
+
23
+ # This only theoretically a read only live conformance test, run at your own
24
+ # peril! Seriously, if you run this on production data and the internet
25
+ # explodes, well, you're stupid, unlucky, and I warned you.
26
+
27
+ describe 'retrieve' do
28
+ it 'returns a Hash with an id' do
29
+ record = NSConnector::Restlet.execute!({
30
+ :action => 'retrieve',
31
+ :type_id => 'contact',
32
+ :fields => ['id', 'firstname', 'lastname'],
33
+ :data => {
34
+ :id => customer_id,
35
+ }
36
+ })
37
+
38
+ expect(record).to be_a(Hash)
39
+ expect(record['id']).to be_a(String)
40
+ expect(record.keys).to include('firstname')
41
+ expect(record.keys).to include('lastname')
42
+ end
43
+ end
44
+
45
+ describe 'search' do
46
+ it 'returns a basic search by id' do
47
+ record = NSConnector::Restlet.execute!(
48
+ :action => 'search',
49
+ :type_id => 'contact',
50
+ :fields => ['id'],
51
+ :data => {
52
+ :filters => [
53
+ ['internalid', nil, 'is', customer_id]
54
+ ]
55
+ }
56
+ )
57
+
58
+ expect(record).to be_a(Array)
59
+ expect(record).to_not be_empty
60
+ expect(record.first['id']).to be_a(String)
61
+ expect(record.first['id']).to_not be_empty
62
+ end
63
+ end
@@ -0,0 +1,384 @@
1
+ // How large a result set we try to return before splitting it up into smaller
2
+ // chunks
3
+ var CHUNK_SIZE = 100;
4
+
5
+ // Okay, so this is some dodgy meta shit that I probably really shouldn't be
6
+ // doing in javascript. It's not my fault the API is odd.
7
+ function apply_constructor(klass, opts) {
8
+ function applicator() {
9
+ // This seems to be the only way to call .apply on a
10
+ // constructor in javascript
11
+ return klass.apply(this, opts);
12
+ }
13
+ applicator.prototype = klass.prototype;
14
+ return new applicator();
15
+ }
16
+
17
+ // Stops execution, sending a HTTP 400 response
18
+ function argument_error(message)
19
+ {
20
+ throw nlapiCreateError('400', message);
21
+ }
22
+
23
+
24
+ // Returns a simple record as an associative array
25
+ function get_record_by_id(type_id, fields, id)
26
+ {
27
+ var record = nlapiLoadRecord(type_id, id);
28
+ var response = {};
29
+
30
+ for(var i = 0; i < fields.length; i++ ) {
31
+ response[fields[i]] = record.getFieldValue(fields[i]);
32
+ }
33
+
34
+ return(response);
35
+ }
36
+
37
+ // In order to update an item, we create a diff of what has actually changed,
38
+ // then commit just those changes.
39
+ function update(request)
40
+ {
41
+ if(!request.data.hasOwnProperty('id')) {
42
+ argument_error('update action requires an id');
43
+ }
44
+
45
+ var diff = {};
46
+ var record = nlapiLoadRecord(request.type_id, request.data.id);
47
+
48
+ for(var field in request.data) {
49
+ if(record.getFieldValue(field) != request.data[field]) {
50
+ diff[field] = request.data[field];
51
+ }
52
+ }
53
+
54
+ for(var field in diff) {
55
+ record.setFieldValue(field, diff[field]);
56
+ }
57
+
58
+ return(get_record_by_id(
59
+ request.type_id,
60
+ request.fields,
61
+ nlapiSubmitRecord(record, true)
62
+ ));
63
+ }
64
+
65
+ // Return an array of hashes representing the result set.
66
+ function retrieve_result_set(results, fields)
67
+ {
68
+ var response = [];
69
+ // Can't seem to get all columns back without running nlapiLoadRecord,
70
+ // I tried a few different ways and netsuite either returned null
71
+ // responses or exploded spectacularly. So, we are inefficient for now.
72
+ //
73
+ // This is what has lead to the whole configurable chunking
74
+ // implementation.
75
+ for(var i = 0; i < results.length; i++ ) {
76
+ var result = results[i];
77
+ response.push(
78
+ get_record_by_id(
79
+ result.getRecordType(),
80
+ fields,
81
+ result.getId()
82
+ )
83
+ );
84
+ }
85
+
86
+ return response;
87
+ }
88
+
89
+ // Return the requested chunk offset, which should be incremented by one for
90
+ // each chunk.
91
+ function search_chunked(search, fields, chunk)
92
+ {
93
+ var offset = chunk * CHUNK_SIZE;
94
+ var results = search.runSearch().getResults(offset, offset + CHUNK_SIZE);
95
+ if(results.length == 0) {
96
+ throw nlapiCreateError('400', 'NO_MORE_CHUNKS');
97
+ }
98
+ return(retrieve_result_set(results, fields));
99
+ }
100
+
101
+ // Called if we aren't specifically asking for a chunk of the result set.
102
+ // Returns:: An array of records of length < CHUNK_SIZE
103
+ // Raises:: A 400, 'CHUNKY_MONKEY' if the result size is >= CHUNK_SIZE, it is
104
+ // expected that you now send a follow up request asking for chunks until
105
+ // you recive another error, 'NO_MORE_CHUNKS'
106
+ function search_no_chunked(search, fields)
107
+ {
108
+ var results = search.runSearch().getResults(0, CHUNK_SIZE);
109
+
110
+ // If there are CHUNK_SIZE results, we need to chunk
111
+ if (results.length == CHUNK_SIZE) {
112
+ // So we send an error message to signify that the client
113
+ // should make a different kind of search request
114
+ throw nlapiCreateError('400', 'CHUNKY_MONKEY');
115
+ }
116
+
117
+ return(retrieve_result_set(results, fields));
118
+ }
119
+
120
+ // Only return the columns requested, not whole objects.
121
+ // Returns:: Array of arrays containing all columns requested.
122
+ function raw_search(request)
123
+ {
124
+ var columns = [];
125
+ var filters = [];
126
+ var response = [];
127
+
128
+ // Generate filters
129
+ for(var i = 0; i < request.data.filters.length; i++ ) {
130
+ filters.push(
131
+ apply_constructor(
132
+ nlobjSearchFilter, request.data.filters[i]
133
+ )
134
+ );
135
+ }
136
+
137
+ // Doesn't work when we try to create columns then supply them at
138
+ // creation, so we create them after we create the search.
139
+ var search = nlapiCreateSearch(request.type_id, filters, []);
140
+
141
+ // Now, for whatever reason, we can add columns.
142
+ for(var i = 0; i < request.data.columns.length; i++ ) {
143
+ columns.push(apply_constructor(
144
+ nlobjSearchColumn, request.data.columns[i]
145
+ ));
146
+ search.addColumn(columns[i]);
147
+ }
148
+
149
+ var search_result = search.runSearch();
150
+ // We go 1000 at a time as that is the maximum we are allowed
151
+ var MAX_PER_GET=1000;
152
+ for(var i = 0; true; i += MAX_PER_GET) {
153
+ var result_set = search_result.getResults(i, MAX_PER_GET);
154
+ for(var j = 0; j < result_set.length; j++) {
155
+ response.push([]);
156
+ for(var k = 0; k < columns.length; k++) {
157
+ response[j].push(
158
+ result_set[j].getValue(columns[k])
159
+ );
160
+ }
161
+ }
162
+ if(result_set.length < MAX_PER_GET) {
163
+ // No more results
164
+ break;
165
+ }
166
+ }
167
+
168
+ return(response);
169
+ }
170
+
171
+ function search(request)
172
+ {
173
+ // Maximum results to return before we split our response into a
174
+ // chunked, multi request response. Note, this has nothing to do with a
175
+ // HTTP chunked response, it's simply us requesting a different offest
176
+ // each time.
177
+
178
+ var filters = [];
179
+ for(var i = 0; i < request.data.filters.length; i++ ) {
180
+ filters.push(
181
+ apply_constructor(
182
+ nlobjSearchFilter, request.data.filters[i]
183
+ )
184
+ );
185
+ }
186
+
187
+ var search = nlapiCreateSearch(request.type_id, filters, []);
188
+
189
+ if(request.data.hasOwnProperty('chunk')){
190
+ return(search_chunked(
191
+ search, request.fields, request.data.chunk
192
+ ));
193
+ } else {
194
+ return search_no_chunked(search, request.fields);
195
+ }
196
+ }
197
+
198
+ // Retrieve a single record by id
199
+ function retrieve(request)
200
+ {
201
+ if(!request.data.hasOwnProperty('id')) {
202
+ argument_error('retrieve action requires an id');
203
+ }
204
+
205
+ return(get_record_by_id(
206
+ request.type_id, request.fields, request.data.id)
207
+ );
208
+ }
209
+
210
+ // Load a list of fields for type specified by type_id
211
+ // Returns:: Array of strings
212
+ // Delete a record by id
213
+ function delete_id(request)
214
+ {
215
+ if(!request.data.hasOwnProperty('id')) {
216
+ argument_error('delete action requires an id');
217
+ }
218
+
219
+ // Return value is moot, should throw an error on failure
220
+ nlapiDeleteRecord(request.type_id, parseInt(request.data.id));
221
+ return([]);
222
+ }
223
+
224
+ // Create a new record
225
+ function create(request)
226
+ {
227
+ var record = nlapiCreateRecord(request.type_id);
228
+ var response = {};
229
+
230
+ for(var field in request.data) {
231
+ record.setFieldValue(field, request.data[field]);
232
+ }
233
+
234
+ for(var i = 0; i < request.fields.length; i++ ) {
235
+ response[request.fields[i]] = record.getFieldValue(
236
+ request.fields[i]
237
+ );
238
+ }
239
+
240
+ return(get_record_by_id(
241
+ request.type_id,
242
+ request.fields,
243
+ nlapiSubmitRecord(record, true)
244
+ ));
245
+ }
246
+
247
+ // Given a record, sublist_id and array of fields, retrieve the whole sublist
248
+ // as an array of hashes
249
+ function get_sublist(record, sublist_id, fields)
250
+ {
251
+ var len = record.getLineItemCount(request.sublist_id);
252
+ var response = [];
253
+ for(var i = 1; i <= len; i++) {
254
+ list_item = {};
255
+ for(var j = 0; j < fields.length; j++) {
256
+ list_item[fields[j]] = record.getLineItemValue(
257
+ sublist_id, fields[j], i
258
+ )
259
+ }
260
+ response.push(list_item);
261
+ }
262
+ return(response);
263
+ }
264
+
265
+ // Basically a wrapper for get_sublist()
266
+ function fetch_sublist(request)
267
+ {
268
+ if(!request.hasOwnProperty('parent_id')) {
269
+ argument_error("Missing mandatory argument: parent_id");
270
+ }
271
+
272
+ if(!request.hasOwnProperty('sublist_id')) {
273
+ argument_error("Missing mandatory argument: sublist_id");
274
+ }
275
+
276
+ var record = nlapiLoadRecord(request.type_id, request.parent_id);
277
+ return(get_sublist(record, request.sublist_id, request.fields));
278
+ }
279
+
280
+ // Make sure we have the required arguments in our request object
281
+ function pre_flight_check(request)
282
+ {
283
+ delete(request['code']);
284
+
285
+ if (!request.hasOwnProperty('action')) {
286
+ argument_error("Missing mandatory argument: action");
287
+ }
288
+
289
+ // Some actions may not care about these
290
+ if (!request.hasOwnProperty('fields')) {
291
+ request.fields = [];
292
+ }
293
+ if (!request.hasOwnProperty('sublists')) {
294
+ request.sublists = {};
295
+ }
296
+ if (!request.hasOwnProperty('data')) {
297
+ request.data = {};
298
+ }
299
+ }
300
+
301
+ // Render a PDF invoice
302
+ //
303
+ // Arguments::
304
+ // invoice_id:: the ID of the invoice to render
305
+ //
306
+ // Returns:: An array with only element, a base64 encoded string of the
307
+ // generated PDF
308
+ function invoice_pdf(request) {
309
+ if(!request.hasOwnProperty('invoice_id')) {
310
+ argument_error('Missing mandatory argument: invoice_id');
311
+ };
312
+
313
+ var file = nlapiPrintRecord(
314
+ 'TRANSACTION',
315
+ request.invoice_id,
316
+ 'PDF',
317
+ null
318
+ );
319
+
320
+ return [file.getValue()];
321
+ }
322
+
323
+ // Attach all customers in data from ourselves
324
+ // Arguments::
325
+ // target_type_id:: target 'type_id'
326
+ // attachee_id:: id of record type 'type_id' to attach from
327
+ // data:: array of ids to attach
328
+ // attributes:: optional attributes
329
+ function attach(request) {
330
+ for(var i = 0; i < request.data.length; i++) {
331
+ nlapiAttachRecord(
332
+ request.type_id,
333
+ request.attachee_id,
334
+ request.target_type_id,
335
+ parseInt(request.data[i]),
336
+ request.attributes
337
+ );
338
+ }
339
+ return([]);
340
+ }
341
+ //
342
+ // Detach all customers in data from ourselves
343
+ // Arguments::
344
+ // target_type_id:: target 'type_id'
345
+ // attachee_id:: id of record type 'type_id' to detach from
346
+ // data:: array of ids to attach
347
+ // attributes:: optional attributes
348
+ function detach(request) {
349
+ for(var i = 0; i < request.data.length; i++) {
350
+ nlapiDetachRecord(
351
+ request.type_id,
352
+ request.attachee_id,
353
+ request.target_type_id,
354
+ parseInt(request.data[i])
355
+ );
356
+ }
357
+ return([]);
358
+ }
359
+
360
+ // As the last visible function, this is the one actually run and the return
361
+ // value is sent back to the client as JSON.
362
+ function main(request)
363
+ {
364
+ pre_flight_check(request);
365
+
366
+ var actions = {
367
+ 'delete' : delete_id, // delete is a reserved word
368
+ 'create' : create,
369
+ 'retrieve' : retrieve,
370
+ 'search' : search,
371
+ 'update' : update,
372
+ 'fetch_sublist' : fetch_sublist,
373
+ 'invoice_pdf' : invoice_pdf,
374
+ 'attach' : attach,
375
+ 'detach' : detach,
376
+ 'raw_search' : raw_search,
377
+ }
378
+
379
+ if(!(request.action in actions)) {
380
+ argument_error("Unknown action: " + request.action);
381
+ }
382
+
383
+ return actions[request.action](request);
384
+ }