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,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
+ }