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
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'http://rubygems.org'
2
+ source 'http://packages.anchor.net.au/gems/'
3
+
4
+ group :development do
5
+ gem 'rspec'
6
+ gem 'webmock'
7
+ gem 'rake'
8
+ gem 'jeweler'
9
+ gem 'bundler'
10
+ gem 'rdoc'
11
+ gem 'pry-debugger'
12
+ gem 'guard-rspec'
13
+ end
@@ -0,0 +1,80 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ remote: http://packages.anchor.net.au/gems/
4
+ specs:
5
+ addressable (2.3.4)
6
+ coderay (1.0.9)
7
+ columnize (0.3.6)
8
+ crack (0.3.2)
9
+ debugger (1.5.0)
10
+ columnize (>= 0.3.1)
11
+ debugger-linecache (~> 1.2.0)
12
+ debugger-ruby_core_source (~> 1.2.0)
13
+ debugger-linecache (1.2.0)
14
+ debugger-ruby_core_source (1.2.0)
15
+ diff-lcs (1.2.4)
16
+ ffi (1.8.1)
17
+ formatador (0.2.4)
18
+ git (1.2.5)
19
+ guard (1.8.0)
20
+ formatador (>= 0.2.4)
21
+ listen (>= 1.0.0)
22
+ lumberjack (>= 1.0.2)
23
+ pry (>= 0.9.10)
24
+ thor (>= 0.14.6)
25
+ guard-rspec (3.0.1)
26
+ guard (>= 1.8)
27
+ rspec (~> 2.13)
28
+ jeweler (1.8.4)
29
+ bundler (~> 1.0)
30
+ git (>= 1.2.5)
31
+ rake
32
+ rdoc
33
+ json (1.8.0)
34
+ listen (1.1.4)
35
+ rb-fsevent (>= 0.9.3)
36
+ rb-inotify (>= 0.9)
37
+ rb-kqueue (>= 0.2)
38
+ lumberjack (1.0.3)
39
+ method_source (0.8.1)
40
+ pry (0.9.12.2)
41
+ coderay (~> 1.0.5)
42
+ method_source (~> 0.8)
43
+ slop (~> 3.4)
44
+ pry-debugger (0.2.2)
45
+ debugger (~> 1.3)
46
+ pry (~> 0.9.10)
47
+ rake (10.0.4)
48
+ rb-fsevent (0.9.3)
49
+ rb-inotify (0.9.0)
50
+ ffi (>= 0.5.0)
51
+ rb-kqueue (0.2.0)
52
+ ffi (>= 0.5.0)
53
+ rdoc (4.0.1)
54
+ json (~> 1.4)
55
+ rspec (2.13.0)
56
+ rspec-core (~> 2.13.0)
57
+ rspec-expectations (~> 2.13.0)
58
+ rspec-mocks (~> 2.13.0)
59
+ rspec-core (2.13.1)
60
+ rspec-expectations (2.13.0)
61
+ diff-lcs (>= 1.1.3, < 2.0)
62
+ rspec-mocks (2.13.1)
63
+ slop (3.4.5)
64
+ thor (0.18.1)
65
+ webmock (1.11.0)
66
+ addressable (>= 2.2.7)
67
+ crack (>= 0.3.2)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ bundler
74
+ guard-rspec
75
+ jeweler
76
+ pry-debugger
77
+ rake
78
+ rdoc
79
+ rspec
80
+ webmock
@@ -0,0 +1,9 @@
1
+ guard :rspec do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/ns_connector/(.+)\.rb$}){ |m|
4
+ puts "#{m[1]}"
5
+ "spec/#{m[1]}_spec.rb"
6
+ }
7
+ watch('spec/spec_helper.rb'){ "spec" }
8
+ end
9
+
data/HACKING ADDED
@@ -0,0 +1,31 @@
1
+ Install your bundle:
2
+
3
+ bundle install
4
+
5
+ Run guard:
6
+
7
+ bundle exec guard
8
+
9
+ Or, run the tests manually:
10
+
11
+ bundle exec rspec
12
+
13
+ * The restlet lives in support/restlet.rb
14
+ * There are basic conformance tests for it in support/
15
+ * You'll want a config in a file somewhere:
16
+
17
+ $ cat tmp/ns_config
18
+ {
19
+ :account_id => '1234',
20
+ :email => 'email@site.com',
21
+ :password => 'secret',
22
+ :role => '5678',
23
+ :restlet_url => 'https://rest.netsuite.com/app/site/hosting/restlet.nl?script=setme&deploy=setme'
24
+ :valid_customer_id => 1234 # A test customer record for conformance tests
25
+ }
26
+
27
+ * All of the development scripts and conformance tests are run the same way,
28
+ something like:
29
+
30
+ scripts/test_shell "$(cat tmp/ns_config)"
31
+
@@ -0,0 +1,7 @@
1
+ Copyright (C) 2013 Christian Marie
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,191 @@
1
+ = NSConnector intro
2
+ This library provides an interface to NetSuite via RESTlets, i.e. SuiteScript .
3
+ This appears to be a quicker and more reliable way of interfacing with NetSuite
4
+ records than the SOAP API.
5
+
6
+ How is this different to the other netsuite connector gems out there?
7
+
8
+ * Basically, it uses a javascript RESTlet to communicate as opposed to the SOAP
9
+ API, so it's completely different and exposes a separate API.
10
+ * It's not built around auto generated WSDL which can make it easier to design
11
+ a usable API.
12
+ * It can be concurrent, so it at least has the potential to scale. You can only
13
+ make one request at a time per user using the SOAP API.
14
+ * It's quicker. Not sure why, but the SOAP API is horrendously slow sometimes,
15
+ RESTlets seem to be quicker, for now.
16
+
17
+ == Features
18
+ * No dependencies
19
+ * Read write for supported types
20
+ * Flexible searching
21
+ * Multithreaded chunked searching for retrieving large datasets
22
+ * ActiveRecord (vaguely) alike interface
23
+ * Read only sublist support.
24
+ * Attaching and detaching records
25
+ * Invoice PDF generation.
26
+
27
+ == Supported NetSuite types
28
+ It's pretty trivial to add a new one yourself. These have been tested to work:
29
+ * Contact
30
+ * Customer
31
+ * Invoice
32
+
33
+ == Installation
34
+ Install the RESTlet:
35
+ function post(request) {
36
+ func = eval(request.code);
37
+ return func(request);
38
+ }
39
+
40
+ That's the whole RESTlet. Deploy it to NetSuite, ensuring that the POST
41
+ function is set to 'post'. Note the 'External URL' when deploying it, that is
42
+ what you will use in the configuration below.
43
+
44
+ == Configuration
45
+ The configuration is stored 'globally' via NSConnector::Config#set_config!
46
+
47
+ An example config:
48
+ NSConnector::Config.set_config!({
49
+ :account_id => '123',
50
+ :email => 'email@site',
51
+ :password => "password",
52
+ :role => '456',
53
+ :restlet_url => 'https://netsuite/restlet',
54
+ })
55
+ === Options
56
+ ==== :account_id (mandatory)
57
+ TODO: Document how to find an account ID.
58
+
59
+ ==== :email (mandatory)
60
+ The email address you log into the NetSuite web site with that matches the
61
+ account_id.
62
+
63
+ ==== :password (mandatory)
64
+ Hopefully a secret.
65
+
66
+ ==== :role (mandatory)
67
+ Can be found in your cookie when logged in, or deep within the jungle of the
68
+ user interface.
69
+ TODO: Document the perilous journey through the jungle.
70
+
71
+ ==== :use_threads (optional)
72
+ A bool, set to false to turn off threading when retrieving large result sets.
73
+ By default we try to split these result sets up into manageable chunks. If you
74
+ turn this off, they will still be split up, but only one chunk will be
75
+ retrieved at a time.
76
+
77
+ ==== :no_threads (optional)
78
+ An integer, the number of threads to use. By default, 4.
79
+
80
+ == CRUD usage
81
+ Every supported type supports full CRUD via the same standard interface
82
+ Check the SuiteScript Records Browser [1] for avaliable fields.
83
+
84
+ === Creating
85
+ To create a record, simply instantiate a new class of that kind and call
86
+ .save! on it.
87
+
88
+ Calling .save! will re-load the saved Record from NetSuite with any other
89
+ changes that NetSuite's internal logic may have made.
90
+
91
+ Example:
92
+
93
+ include NSConnector
94
+
95
+ c = Contact.new(:firstname => 'name')
96
+ => <#NSConnector::Contact:nil>
97
+
98
+ c.fields
99
+ => [fields...]
100
+
101
+ c.lastname = 'abc'
102
+ => 'abc'
103
+
104
+ c.save!
105
+ => true
106
+
107
+ c.lastname
108
+ => 'Abc'
109
+
110
+ === Reading
111
+ You can find by any field or by internalId.
112
+ Example:
113
+
114
+ include NSConnector
115
+
116
+ # Search by any internal field ID, returns an array of Contacts
117
+ Contact.search_by('entityId', 42)
118
+ => [<#NSConnector::Contact:12>]
119
+
120
+ # Fetch one Contact by internalId
121
+ Contact.find(12)
122
+ => <#NSConnector::Contact:12>
123
+
124
+ # Fetch all Contacts, this will take a while. (Request will be broken
125
+ # up into smaller ones and spread across multiple threads).
126
+ Contact.all
127
+ => [rather large array of NSConnector::Contact]
128
+
129
+ You can also perform more complex searces.
130
+ Example:
131
+ Contact.advanced_search([
132
+ ['email', 'contains', '@'],
133
+ ['entityId', 'lessthanorequalto', '1000']
134
+ ])
135
+ => [<#NSConnector::Contact:12>, <#NS...]
136
+
137
+ At any time you can check which fields are avaliable in both the class and
138
+ instances:
139
+ Contact.fields
140
+ Contact.new.fields
141
+ You can also access the raw data store to more easily see what the object
142
+ contains with:
143
+ Contact.find(1234).store
144
+ ==== SubLists
145
+ SubLists are exposed differently by the backend API, but that's pretty
146
+ transparent to us, simply access them as a read only array:
147
+ Contact.addressbook.first.city
148
+ You can see which sublists are avaliable to an object via the sublists
149
+ accessor:
150
+ Contact.sublists
151
+ Customer.new.sublists
152
+
153
+ === Updating
154
+ Updating records is much like creating new ones. Simply:
155
+ 1. Grab the record (see Reading).
156
+ c = Contact.find(12)
157
+ 2. Update the record to your liking.
158
+ c.lastname = 'newname'
159
+ 3. Save the record (see Creating)
160
+ c.save!
161
+ => true
162
+
163
+ === Deleting
164
+ Deletion can be done by ID or Record.
165
+ Example:
166
+
167
+ 1. By ID
168
+ Contact.delete!(42)
169
+ => true
170
+ 2. By Record
171
+ c = Contact.find(42)
172
+ c.delete!
173
+
174
+ === Troubleshooting note:
175
+ Should you run into an error something like:
176
+
177
+ NSConnector::Restlet::RuntimeError: Failed to parse response from Restlet as
178
+ JSON (): A JSON text must at least contain two octets!
179
+
180
+ It may just be that your user is being denied access, in which case netsuite
181
+ seems to believes it is a good idea to simply send a blank response.
182
+
183
+
184
+ == Development notes
185
+ See HACKING in the repo
186
+
187
+ == License
188
+ MIT
189
+
190
+ == References
191
+ [1] {SuiteScript Records Browser}[https://system.netsuite.com/help/helpcenter/en_US/RecordsBrowser/2013_1/index.html]
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ warn e.message
7
+ warn "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+
11
+ require 'rdoc/task'
12
+ require 'jeweler'
13
+ require 'rspec/core/rake_task'
14
+
15
+ Jeweler::Tasks.new do |gem|
16
+ gem.name = "ns_connector"
17
+ gem.homepage = ""
18
+ gem.summary = "An interface to NetSuite records via RESTlets."
19
+ gem.description = "This library provides an interface to NetSuite via"\
20
+ "'RESTlets'. This appears to be a quicker and more reliable"\
21
+ "way of interfacing with NetSuite records than the SOAP API."
22
+ gem.authors = ["Christian Marie <pingu@anchor.com.au>"]
23
+ gem.license = 'MIT'
24
+ end
25
+ Jeweler::RubygemsDotOrgTasks.new
26
+
27
+ task :default => :test
28
+ RSpec::Core::RakeTask.new :test
29
+
30
+ task :deploy_to_hopper => [:build, :rdoc, :test] do
31
+ `scp pkg/ns_connector-$(cat VERSION).gem packages@hopper.engineroom.anchor.net.au:public_html/gems/gems/`
32
+ `ssh packages@hopper.engineroom.anchor.net.au "cd /home/packages/public_html/gems && make"`
33
+ if $?.to_i == 0 then
34
+ puts "Deploy to hopper successful"
35
+ else
36
+ raise RuntimeError, "Deploy failed :("
37
+ end
38
+ end
39
+
40
+ Rake::RDocTask.new do |rd|
41
+ rd.main = "README.rdoc"
42
+ rd.title = 'NSConnector documentation'
43
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
44
+ `scp -r html/* packages@hopper.engineroom.anchor.net.au:public_html/gems/docs/ns_connector/`
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.6
@@ -0,0 +1,4 @@
1
+ module NSConnector
2
+ end
3
+
4
+ require 'ns_connector/resources'
@@ -0,0 +1,42 @@
1
+ # Provide attach! and detach! methods
2
+ module NSConnector::Attaching
3
+ # Attach any number of ids to klass
4
+ # Arguments::
5
+ # klass:: target class to attach to, i.e. Contact
6
+ # attachee_id:: internal id of the record to make the attach(s) to
7
+ # ids:: array of target ids
8
+ # attributes:: optional attributes for the attach, i.e. {:role => -5}
9
+ def attach!(klass, attachee_id, ids, attributes = nil)
10
+ unless ids.kind_of?(Array) then
11
+ raise ::ArgumentError,
12
+ 'Expected ids to be an array'
13
+ end
14
+ NSConnector::Restlet.execute!(
15
+ :action => 'attach',
16
+ :type_id => type_id,
17
+ :target_type_id => klass.type_id,
18
+ :attachee_id => attachee_id,
19
+ :attributes => attributes,
20
+ :data => ids
21
+ )
22
+ end
23
+
24
+ # Unattach any number of ids to klass
25
+ # Arguments::
26
+ # klass:: target class to detach from, i.e. Contact
27
+ # attachee_id:: internal id of the record to make the detach(s) from
28
+ # ids:: array of target class ids
29
+ def detach!(klass, attachee_id, ids)
30
+ unless ids.kind_of?(Array) then
31
+ raise ::ArgumentError,
32
+ 'Expected ids to be an array'
33
+ end
34
+ NSConnector::Restlet.execute!(
35
+ :action => 'detach',
36
+ :type_id => type_id,
37
+ :target_type_id => klass.type_id,
38
+ :attachee_id => attachee_id,
39
+ :data => ids
40
+ )
41
+ end
42
+ end
@@ -0,0 +1,111 @@
1
+ # Provide threaded and non-threaded chunked searching
2
+ module NSConnector::ChunkedSearching
3
+ # Retrieve a single chunk, this makes one HTTP connection
4
+ # Raises:: NSConnector::Errors::EndChunking when there's no more chunks
5
+ # Returns:: Resource objects
6
+ def grab_chunk(filters, chunk)
7
+ NSConnector::Restlet.execute!(
8
+ :action => 'search',
9
+ :type_id => type_id,
10
+ :data => {
11
+ :filters => filters,
12
+ :chunk => chunk,
13
+ }
14
+ ).map do |upstream_store|
15
+ self.new(upstream_store)
16
+ end
17
+ end
18
+
19
+ # The basic logic here is, given four threads we have four workers,
20
+ # those workers keep eating chunks of data specified by the master.
21
+ # When a worker recieves a EndChunking error, it flags done as true and
22
+ # everyone wraps up thier work. Pretty simple.
23
+ def threaded_search_by_chunks(filters)
24
+ require 'thread'
25
+ threads = NSConnector::Config[:no_threads].to_i
26
+ if threads < 1 then
27
+ raise NSConnector::Config::ArgumentError,
28
+ "Need more than #{threads} threads"
29
+ end
30
+
31
+ # We bother pre-populating the queue here because locking is
32
+ # super expensive, on my build of ruby at least.
33
+ queue = Queue.new
34
+ (threads - 1).times do |i|
35
+ queue << i
36
+ end
37
+
38
+ mutex = Mutex.new
39
+
40
+ workers = []
41
+ results = []
42
+ current_chunk = threads - 1
43
+ done = false
44
+
45
+ # Workers
46
+ threads.times do
47
+ workers << Thread.new do
48
+ until done
49
+ begin
50
+ # Avoid a deadlock by popping
51
+ # off -1 to exit
52
+ chunk = queue.pop
53
+ break if chunk == -1
54
+
55
+ result = grab_chunk(
56
+ filters, chunk
57
+ )
58
+ rescue NSConnector::Errors::EndChunking
59
+ done = true
60
+ break
61
+ end
62
+
63
+ mutex.synchronize do
64
+ results += result
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # Master
71
+ until done
72
+ if queue.empty? then
73
+ queue << current_chunk
74
+ current_chunk += 1
75
+ end
76
+ end
77
+
78
+ threads.times do
79
+ queue << -1
80
+ end
81
+
82
+ workers.each do |worker|
83
+ worker.join
84
+ end
85
+
86
+ return results
87
+ end
88
+
89
+ # Just keep grabbing incremental chunks till we're told to stop.
90
+ def normal_search_by_chunks(filters)
91
+ results = []
92
+ chunk = 0
93
+ while true
94
+ begin
95
+ results += grab_chunk(filters, chunk)
96
+ chunk += 1
97
+ end
98
+ end
99
+ rescue NSConnector::Errors::EndChunking
100
+ return results
101
+ end
102
+
103
+ # Search by requesting chunks
104
+ def search_by_chunks filters
105
+ if NSConnector::Config[:use_threads] then
106
+ return threaded_search_by_chunks(filters)
107
+ else
108
+ return normal_search_by_chunks(filters)
109
+ end
110
+ end
111
+ end