ns_connector 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/Gemfile.lock +80 -0
- data/Guardfile +9 -0
- data/HACKING +31 -0
- data/LICENSE.txt +7 -0
- data/README.rdoc +191 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/lib/ns_connector.rb +4 -0
- data/lib/ns_connector/attaching.rb +42 -0
- data/lib/ns_connector/chunked_searching.rb +111 -0
- data/lib/ns_connector/config.rb +66 -0
- data/lib/ns_connector/errors.rb +79 -0
- data/lib/ns_connector/field_store.rb +19 -0
- data/lib/ns_connector/hash.rb +11 -0
- data/lib/ns_connector/resource.rb +288 -0
- data/lib/ns_connector/resources.rb +3 -0
- data/lib/ns_connector/resources/contact.rb +279 -0
- data/lib/ns_connector/resources/customer.rb +355 -0
- data/lib/ns_connector/resources/invoice.rb +466 -0
- data/lib/ns_connector/restlet.rb +137 -0
- data/lib/ns_connector/sublist.rb +21 -0
- data/lib/ns_connector/sublist_item.rb +25 -0
- data/misc/failed_sublist_saving_patch +547 -0
- data/scripts/run_restlet +25 -0
- data/scripts/test_shell +21 -0
- data/spec/attaching_spec.rb +48 -0
- data/spec/chunked_searching_spec.rb +75 -0
- data/spec/config_spec.rb +43 -0
- data/spec/resource_spec.rb +340 -0
- data/spec/resources/contact_spec.rb +8 -0
- data/spec/resources/customer_spec.rb +8 -0
- data/spec/resources/invoice_spec.rb +20 -0
- data/spec/restlet_spec.rb +135 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/sublist_item_spec.rb +25 -0
- data/spec/sublist_spec.rb +45 -0
- data/spec/support/mock_data.rb +10 -0
- data/support/read_only_test +63 -0
- data/support/restlet.js +384 -0
- data/support/super_dangerous_write_test +85 -0
- metadata +221 -0
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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,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
|
data/support/restlet.js
ADDED
@@ -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
|
+
}
|