bulkforce 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/Gemfile +14 -0
- data/Guardfile +8 -0
- data/LICENSE +9 -0
- data/README.md +211 -0
- data/Rakefile +6 -0
- data/bulkforce.gemspec +35 -0
- data/lib/bulkforce.rb +109 -0
- data/lib/bulkforce/batch.rb +61 -0
- data/lib/bulkforce/configuration.rb +42 -0
- data/lib/bulkforce/connection.rb +114 -0
- data/lib/bulkforce/connection_builder.rb +40 -0
- data/lib/bulkforce/helper.rb +77 -0
- data/lib/bulkforce/http.rb +272 -0
- data/lib/bulkforce/version.rb +3 -0
- data/spec/integration/delete_spec.rb +18 -0
- data/spec/integration/insert_spec.rb +22 -0
- data/spec/integration/query_spec.rb +25 -0
- data/spec/integration/update_spec.rb +18 -0
- data/spec/integration/upsert_spec.rb +22 -0
- data/spec/lib/bulkforce/batch_spec.rb +128 -0
- data/spec/lib/bulkforce/configuration_spec.rb +121 -0
- data/spec/lib/bulkforce/connection_builder_spec.rb +114 -0
- data/spec/lib/bulkforce/connection_spec.rb +99 -0
- data/spec/lib/bulkforce/helper_spec.rb +249 -0
- data/spec/lib/bulkforce/http_spec.rb +375 -0
- data/spec/lib/bulkforce_spec.rb +128 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/integration_helpers.rb +64 -0
- data/spec/support/shared_examples.rb +21 -0
- metadata +199 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe "Bulk Delete", type: :integration do
|
5
|
+
let!(:result) { delete_contacts }
|
6
|
+
|
7
|
+
it "returns a Hash" do
|
8
|
+
expect(result).to be_a(Hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "sets state to 'Completed'" do
|
12
|
+
expect(result[:state]).to eq("Completed")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has no failed records" do
|
16
|
+
expect(result[:number_records_failed]).to eq("0")
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe "Bulk Insert", type: :integration do
|
5
|
+
let!(:result) { insert_contacts }
|
6
|
+
|
7
|
+
it "returns a Hash" do
|
8
|
+
expect(result).to be_a(Hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "sets state to 'Completed'" do
|
12
|
+
expect(result[:state]).to eq("Completed")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "sets correct number of records processed" do
|
16
|
+
expect(result[:number_records_processed]).to eq("2")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "has no failed records" do
|
20
|
+
expect(result[:number_records_failed]).to eq("0")
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe "Bulk Query", type: :integration do
|
5
|
+
let!(:result) { client.query("Contact", "select Id from Contact limit 10").freeze }
|
6
|
+
|
7
|
+
let(:size) { 10 }
|
8
|
+
|
9
|
+
it "returns a hash" do
|
10
|
+
expect(result).to be_a(Hash)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "state is 'Completed'" do
|
14
|
+
expect(result[:state]).to eq("Completed")
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
it "returns an array of results" do
|
19
|
+
expect(result[:results]).to be_a(Array)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "has correct size of results" do
|
23
|
+
expect(result[:results].count).to eq(size)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe "Bulk Update", type: :integration do
|
5
|
+
let!(:result) { update_contacts }
|
6
|
+
|
7
|
+
it "returns a Hash" do
|
8
|
+
expect(result).to be_a(Hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "sets state to 'Completed'" do
|
12
|
+
expect(result[:state]).to eq("Completed")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has no failed records" do
|
16
|
+
expect(result[:number_records_failed]).to eq("0")
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe "Bulk Upsert", type: :integration do
|
5
|
+
let!(:result) { upsert_contacts; upsert_contacts }
|
6
|
+
|
7
|
+
it "returns a Hash" do
|
8
|
+
expect(result).to be_a(Hash)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "sets state to 'Completed'" do
|
12
|
+
expect(result[:state]).to eq("Completed")
|
13
|
+
end
|
14
|
+
|
15
|
+
it "sets correct number of records processed" do
|
16
|
+
expect(result[:number_records_processed]).to eq("2")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "has no failed records" do
|
20
|
+
expect(result[:number_records_failed]).to eq("0")
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Bulkforce::Batch do
|
5
|
+
describe "#final_status" do
|
6
|
+
it "returns the final status if it already exists" do
|
7
|
+
b = described_class.new nil, nil, nil
|
8
|
+
expected_status = {w: :tf}
|
9
|
+
expect(b).not_to receive(:status)
|
10
|
+
b.instance_variable_set "@final_status", expected_status
|
11
|
+
expect(b.final_status).to eq(expected_status)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "returns specific final status for -1 batch id" do
|
15
|
+
b = described_class.new nil, nil, -1
|
16
|
+
expected_status = {
|
17
|
+
state: "Completed",
|
18
|
+
state_message: "Empty Request",
|
19
|
+
}
|
20
|
+
expect(b).not_to receive(:status)
|
21
|
+
expect(b.final_status).to eq(expected_status)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "queries the status correctly" do
|
25
|
+
b = described_class.new nil, nil, nil
|
26
|
+
expect(b).to receive(:status).once.and_return({w: :tf})
|
27
|
+
# TODO lookup the actual result
|
28
|
+
expect(b).to receive(:results).once.and_return({g: :tfo})
|
29
|
+
expect(b.final_status).to eq({w: :tf, results: {g: :tfo}})
|
30
|
+
end
|
31
|
+
|
32
|
+
it "yields status correctly" do
|
33
|
+
expected_running_state = {
|
34
|
+
state: "InProgress"
|
35
|
+
}
|
36
|
+
expected_final_state = {
|
37
|
+
state: "Completed"
|
38
|
+
}
|
39
|
+
b = described_class.new nil, nil, nil
|
40
|
+
expect(b).to receive(:status).once.and_return(expected_running_state)
|
41
|
+
expect(b).to receive(:status).once.and_return(expected_running_state)
|
42
|
+
expect(b).to receive(:status).once.and_return(expected_final_state)
|
43
|
+
expect(b).to receive(:results).once.and_return({g: :tfo})
|
44
|
+
expect{|blk| b.final_status(0, &blk)}
|
45
|
+
.to yield_successive_args(expected_running_state, expected_final_state)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "raises exception when batch fails" do
|
49
|
+
b = described_class.new nil, nil, nil
|
50
|
+
expected_error_message = "Generic Error Message"
|
51
|
+
expect(b).to receive(:status).once.and_return(
|
52
|
+
{
|
53
|
+
state: "Failed",
|
54
|
+
state_message: expected_error_message})
|
55
|
+
expect{b.final_status}
|
56
|
+
.to raise_error(StandardError, expected_error_message)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
[:request, :result].each do |action|
|
61
|
+
let(:connection) { double("Bulkforce::Connection") }
|
62
|
+
let(:request_result) { "Generic Result/Request" }
|
63
|
+
describe "#raw_#{action}" do
|
64
|
+
|
65
|
+
it "sends correct messages to connection" do
|
66
|
+
b = described_class.new nil, nil, nil
|
67
|
+
b.instance_variable_set "@connection", connection
|
68
|
+
expect(connection).to receive(:"raw_#{action}").and_return(request_result)
|
69
|
+
expect(b.send(:"raw_#{action}")).to eq(request_result)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#results" do
|
75
|
+
let(:job_id) { "12345" }
|
76
|
+
let(:batch_id) { "67890" }
|
77
|
+
let(:connection) { double("connection") }
|
78
|
+
subject {described_class.new connection, job_id, batch_id}
|
79
|
+
|
80
|
+
context "with a single page of results" do
|
81
|
+
let(:result_id) {"M75200000001Vgt"}
|
82
|
+
let(:single_result) {{:Id=>"M75200000001Vgt", :name=>"Joe"}}
|
83
|
+
|
84
|
+
it "returns the results" do
|
85
|
+
expect(connection).to receive(:query_batch_result_id).
|
86
|
+
with(job_id, batch_id).
|
87
|
+
and_return({:result => result_id})
|
88
|
+
|
89
|
+
expect(connection).to receive(:query_batch_result_data).
|
90
|
+
once.
|
91
|
+
with(job_id, batch_id, result_id).
|
92
|
+
and_return(single_result)
|
93
|
+
|
94
|
+
expect(subject.results).to eq([single_result])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "with an array of page of results" do
|
99
|
+
let(:result_ids) {["M75200000001Vgt", "M76500000001Vgt", "M73400000001Vgt"]}
|
100
|
+
let(:multiple_results) {[{:Id=>"AAA11123", :name=>"Joe"},
|
101
|
+
{:Id=>"AAA11124", :name=>"Sam"},
|
102
|
+
{:Id=>"AAA11125", :name=>"Mike"}]}
|
103
|
+
|
104
|
+
it "returns concatenated results" do
|
105
|
+
expect(connection).to receive(:query_batch_result_id).
|
106
|
+
with(job_id, batch_id).
|
107
|
+
and_return({:result => result_ids})
|
108
|
+
|
109
|
+
expect(connection).to receive(:query_batch_result_data).
|
110
|
+
ordered.
|
111
|
+
with(job_id, batch_id, result_ids[0]).
|
112
|
+
and_return(multiple_results[0])
|
113
|
+
|
114
|
+
expect(connection).to receive(:query_batch_result_data).
|
115
|
+
ordered.
|
116
|
+
with(job_id, batch_id, result_ids[1]).
|
117
|
+
and_return(multiple_results[1])
|
118
|
+
|
119
|
+
expect(connection).to receive(:query_batch_result_data).
|
120
|
+
ordered.
|
121
|
+
with(job_id, batch_id, result_ids[2]).
|
122
|
+
and_return(multiple_results[2])
|
123
|
+
|
124
|
+
expect(subject.results).to eq(multiple_results)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Bulkforce::Configuration do
|
5
|
+
subject { described_class.new }
|
6
|
+
|
7
|
+
context "api_version" do
|
8
|
+
include_examples "configuration settings", name: :api_version, expected_default: "33.0", env_variable: "SALESFORCE_API_VERSION"
|
9
|
+
end
|
10
|
+
|
11
|
+
context "username" do
|
12
|
+
include_examples "configuration settings", name: :username, expected_default: nil, env_variable: "SALESFORCE_USERNAME"
|
13
|
+
end
|
14
|
+
|
15
|
+
context "password" do
|
16
|
+
include_examples "configuration settings", name: :password, expected_default: nil, env_variable: "SALESFORCE_PASSWORD"
|
17
|
+
end
|
18
|
+
|
19
|
+
context "security_token" do
|
20
|
+
include_examples "configuration settings", name: :security_token, expected_default: nil, env_variable: "SALESFORCE_SECURITY_TOKEN"
|
21
|
+
end
|
22
|
+
|
23
|
+
context "host" do
|
24
|
+
include_examples "configuration settings", name: :host, expected_default: "login.salesforce.com", env_variable: "SALESFORCE_HOST"
|
25
|
+
end
|
26
|
+
|
27
|
+
context "session_id" do
|
28
|
+
include_examples "configuration settings", name: :session_id, expected_default: nil, env_variable: "SALESFORCE_SESSION_ID"
|
29
|
+
end
|
30
|
+
|
31
|
+
context "instance" do
|
32
|
+
include_examples "configuration settings", name: :instance, expected_default: nil, env_variable: "SALESFORCE_INSTANCE"
|
33
|
+
end
|
34
|
+
|
35
|
+
context "client_id" do
|
36
|
+
include_examples "configuration settings", name: :client_id, expected_default: nil, env_variable: "SALESFORCE_CLIENT_ID"
|
37
|
+
end
|
38
|
+
|
39
|
+
context "client_secret" do
|
40
|
+
include_examples "configuration settings", name: :client_secret, expected_default: nil, env_variable: "SALESFORCE_CLIENT_SECRET"
|
41
|
+
end
|
42
|
+
|
43
|
+
context "refresh_token" do
|
44
|
+
include_examples "configuration settings", name: :refresh_token, expected_default: nil, env_variable: "SALESFORCE_REFRESH_TOKEN"
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#to_h" do
|
48
|
+
let(:api_version) { "configuration_spec_api_version" }
|
49
|
+
let(:username) { "configuration_spec_user" }
|
50
|
+
let(:password) { "configuration_spec_password" }
|
51
|
+
let(:security_token) { "configuration_spec_security_token" }
|
52
|
+
let(:host) { "configuration_spec_host" }
|
53
|
+
let(:session_id) { "00Dx0000000BV7z!AR8AQP0jITN80ESEsj5EbaZTFG0RNBaT1cyWk7TrqoDjoNIWQ2ME_sTZzBjfmOE6zMHq6y8PIW4eWze9JksNEkWUl.Cju7m4" }
|
54
|
+
let(:instance) { "eu2" }
|
55
|
+
let(:client_id) { "3MVG9lKcPoNINVBIPJjdw1J9LLM82HnFVVX19KY1uA5mu0QqEWhqKpoW3svG3XHrXDiCQjK1mdgAvhCscA9GE&client_secret=1955279925675241571" }
|
56
|
+
let(:client_secret) { "3MVG9lKcPoNINVBIPJjdw1J9LLM82HnFVVX19KY1uA5mu0QqEWhqKpoW3svG3XHrXDiCQjK1mdgAvhCscA9GE&client_secret=1955279925675241571" }
|
57
|
+
let(:refresh_token) { "Ytwns3AKGIlTka3v9f6Md4kvZsMA9xNgMqVWdaNvBkfUaE7N6TbyVGEZ5eazoHJsa9RVgC5YvdbmPGeSZQNe3A" }
|
58
|
+
|
59
|
+
context "all values set" do
|
60
|
+
subject do
|
61
|
+
described_class.new.tap do |c|
|
62
|
+
c.api_version= api_version
|
63
|
+
c.username = username
|
64
|
+
c.password = password
|
65
|
+
c.security_token = security_token
|
66
|
+
c.host = host
|
67
|
+
c.session_id = session_id
|
68
|
+
c.instance = instance
|
69
|
+
c.client_id = client_id
|
70
|
+
c.client_secret = client_secret
|
71
|
+
c.refresh_token = refresh_token
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
let(:expected_hash) do
|
76
|
+
{
|
77
|
+
api_version: api_version,
|
78
|
+
username: username,
|
79
|
+
password: password,
|
80
|
+
security_token: security_token,
|
81
|
+
host: host,
|
82
|
+
session_id: session_id,
|
83
|
+
instance: instance,
|
84
|
+
client_id: client_id,
|
85
|
+
client_secret: client_secret,
|
86
|
+
refresh_token: refresh_token,
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
it "converts to expected hash" do
|
91
|
+
expect(subject.to_h).to eq(expected_hash)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "some values missing" do
|
96
|
+
subject do
|
97
|
+
described_class.new.tap do |c|
|
98
|
+
c.api_version= api_version
|
99
|
+
c.username = username
|
100
|
+
c.password = password
|
101
|
+
c.security_token = security_token
|
102
|
+
c.host = host
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
let(:expected_hash) do
|
107
|
+
{
|
108
|
+
api_version: api_version,
|
109
|
+
username: username,
|
110
|
+
password: password,
|
111
|
+
security_token: security_token,
|
112
|
+
host: host,
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
it "converts to expected hash" do
|
117
|
+
expect(subject.to_h).to eq(expected_hash)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Bulkforce::ConnectionBuilder do
|
5
|
+
describe "#build" do
|
6
|
+
subject { described_class.new(options) }
|
7
|
+
let!(:session_id) { "org_id!session_id" }
|
8
|
+
let!(:instance) { "eu2" }
|
9
|
+
|
10
|
+
before(:each) do
|
11
|
+
allow(Bulkforce::Http).to receive(:login).and_return(session_id: "org_id!session_id", instance: "eu2")
|
12
|
+
allow(Bulkforce::Http).to receive(:oauth_login).and_return(session_id: "org_id!session_id", instance: "eu2")
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:host) { "login.salesforce.com" }
|
16
|
+
let(:api_version) { "33.0" }
|
17
|
+
let(:connection) { subject.build }
|
18
|
+
|
19
|
+
context "username/password" do
|
20
|
+
let(:options) do
|
21
|
+
{
|
22
|
+
host: host,
|
23
|
+
username: username,
|
24
|
+
password: password,
|
25
|
+
security_token: security_token,
|
26
|
+
api_version: api_version,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:username) { "leif@home.com" }
|
31
|
+
let(:password) { "password" }
|
32
|
+
let(:security_token) { "security_token" }
|
33
|
+
|
34
|
+
it "returns a connection" do
|
35
|
+
expect(connection).to be_a(Bulkforce::Connection)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "triggers login with username, password and security token" do
|
39
|
+
subject.build
|
40
|
+
expect(Bulkforce::Http).
|
41
|
+
to have_received(:login).
|
42
|
+
with(host, username, "#{password}#{security_token}", api_version)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "builds connection with correct attributes", :aggregate_failures do
|
46
|
+
expect(connection.instance_variable_get("@session_id")).to eq(session_id)
|
47
|
+
expect(connection.instance_variable_get("@instance")).to eq(instance)
|
48
|
+
expect(connection.instance_variable_get("@api_version")).to eq(api_version)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "session id" do
|
53
|
+
let(:options) do
|
54
|
+
{
|
55
|
+
host: host,
|
56
|
+
session_id: session_id,
|
57
|
+
instance: instance,
|
58
|
+
api_version: api_version,
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
let(:session_id) { "00Dx0000000BV7z!AR8AQP0jITN80ESEsj5EbaZTFG0RNBaT1cyWk7TrqoDjoNIWQ2ME_sTZzBjfmOE6zMHq6y8PIW4eWze9JksNEkWUl.Cju7m4" }
|
63
|
+
let(:instance) { "eu2" }
|
64
|
+
|
65
|
+
it "returns a connection" do
|
66
|
+
expect(subject.build).to be_a(Bulkforce::Connection)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "does not trigger HTTP login workflow" do
|
70
|
+
subject.build
|
71
|
+
expect(Bulkforce::Http).not_to have_received(:login)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "builds connection with correct attributes", :aggregate_failures do
|
75
|
+
expect(connection.instance_variable_get("@session_id")).to eq(session_id)
|
76
|
+
expect(connection.instance_variable_get("@instance")).to eq(instance)
|
77
|
+
expect(connection.instance_variable_get("@api_version")).to eq(api_version)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "oauth refresh token" do
|
82
|
+
let(:options) do
|
83
|
+
{
|
84
|
+
host: host,
|
85
|
+
client_id: client_id,
|
86
|
+
client_secret: client_secret,
|
87
|
+
refresh_token: refresh_token,
|
88
|
+
api_version: api_version,
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
let(:client_id) { "3MVG9lKcPoNINVBIPJjdw1J9LLM82HnFVVX19KY1uA5mu0QqEWhqKpoW3svG3XHrXDiCQjK1mdgAvhCscA9GE&client_secret=1955279925675241571" }
|
93
|
+
let(:client_secret) { "3MVG9lKcPoNINVBIPJjdw1J9LLM82HnFVVX19KY1uA5mu0QqEWhqKpoW3svG3XHrXDiCQjK1mdgAvhCscA9GE&client_secret=1955279925675241571" }
|
94
|
+
let(:refresh_token) { "Ytwns3AKGIlTka3v9f6Md4kvZsMA9xNgMqVWdaNvBkfUaE7N6TbyVGEZ5eazoHJsa9RVgC5YvdbmPGeSZQNe3A" }
|
95
|
+
|
96
|
+
it "returns a connection" do
|
97
|
+
expect(connection).to be_a(Bulkforce::Connection)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "triggers login with username, password and security token" do
|
101
|
+
subject.build
|
102
|
+
expect(Bulkforce::Http).
|
103
|
+
to have_received(:oauth_login).
|
104
|
+
with(host, client_id, client_secret, refresh_token)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "builds connection with correct attributes", :aggregate_failures do
|
108
|
+
expect(connection.instance_variable_get("@session_id")).to eq(session_id)
|
109
|
+
expect(connection.instance_variable_get("@instance")).to eq(instance)
|
110
|
+
expect(connection.instance_variable_get("@api_version")).to eq(api_version)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|