Initial commit
This commit is contained in:
BIN
services/sql-api/spec/fixtures/sql_api_binary.bin
vendored
Normal file
BIN
services/sql-api/spec/fixtures/sql_api_binary.bin
vendored
Normal file
Binary file not shown.
1
services/sql-api/spec/fixtures/sql_api_error.json
vendored
Normal file
1
services/sql-api/spec/fixtures/sql_api_error.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"error":["relation \"wadus\" does not exist"]}
|
||||
1
services/sql-api/spec/fixtures/sql_api_private.json
vendored
Normal file
1
services/sql-api/spec/fixtures/sql_api_private.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"error":["permission denied for relation untitled_table_1"]}
|
||||
1
services/sql-api/spec/fixtures/sql_api_success.json
vendored
Normal file
1
services/sql-api/spec/fixtures/sql_api_success.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"time":0.001,"fields":{"cartodb_id":{"type":"number"},"description":{"type":"string"}},"total_rows":4,"rows":[{"cartodb_id":1,"description":"a"},{"cartodb_id":2,"description":"b"},{"cartodb_id":3,"description":"c"},{"cartodb_id":4,"description":"d"}]}
|
||||
71
services/sql-api/spec/sql_api_spec.rb
Normal file
71
services/sql-api/spec/sql_api_spec.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
require_relative '../sql_api.rb'
|
||||
require_relative '../../../spec/rspec_configuration.rb'
|
||||
|
||||
module Cartodb; end
|
||||
|
||||
describe CartoDB::SQLApi do
|
||||
|
||||
before(:each) do
|
||||
Cartodb.stubs(:config).returns(TEST_SQL_API_CONFIG)
|
||||
end
|
||||
|
||||
describe '#fetch' do
|
||||
|
||||
let(:api) { CartoDB::SQLApi.new(username: 'maloshumos') }
|
||||
|
||||
it "returns an array of rows" do
|
||||
stub_api_request 200, 'sql_api_success.json'
|
||||
result = api.fetch("SELECT cartodb_id, description from public_table")
|
||||
result.should eq [{"cartodb_id"=>1, "description"=>"a"}, {"cartodb_id"=>2, "description"=>"b"}, {"cartodb_id"=>3, "description"=>"c"}, {"cartodb_id"=>4, "description"=>"d"}]
|
||||
end
|
||||
|
||||
it "raises PermissionError when the table is private" do
|
||||
stub_api_request 400, 'sql_api_private.json'
|
||||
expect { api.fetch("SELECT * FROM private_table") }.to raise_error(CartoDB::SQLApi::PermissionError)
|
||||
end
|
||||
|
||||
it "raises SQLError when the query is flawed" do
|
||||
stub_api_request 400, 'sql_api_error.json'
|
||||
expect { api.fetch("wrong query") }.to raise_error(CartoDB::SQLApi::SQLError)
|
||||
end
|
||||
|
||||
it "handles gzipped output" do
|
||||
stub_api_request 200, 'sql_api_binary.bin'
|
||||
result = api.fetch("SELECT description from public_table", 'csv')
|
||||
result.should match /description,\r\n\"Pretend that you’ve opened this book/
|
||||
result.should match /And if not, then the onion will make it all happen for you.*$/
|
||||
end
|
||||
|
||||
end #fetch
|
||||
|
||||
|
||||
def stub_api_request(code, response_file)
|
||||
response = File.open(path_to(response_file)).read
|
||||
Typhoeus.stub(/.*carto.com\/api\/v[12]/).and_return(
|
||||
Typhoeus::Response.new(code: code, body: response)
|
||||
)
|
||||
end # stub_api_request
|
||||
|
||||
def path_to(filepath = '')
|
||||
File.expand_path(
|
||||
File.join(File.dirname(__FILE__), "../spec/fixtures/#{filepath}")
|
||||
)
|
||||
end #path_to
|
||||
|
||||
TEST_SQL_API_CONFIG = {:sql_api => {
|
||||
"private" => {
|
||||
"protocol" => 'http',
|
||||
"domain" => 'carto.com',
|
||||
"endpoint" => '/api/v1/sql',
|
||||
"port" => 8080
|
||||
},
|
||||
"public" => {
|
||||
"protocol" => 'http',
|
||||
"domain" => 'carto.com',
|
||||
"endpoint" => '/api/v2/sql',
|
||||
"port" => 8080
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
end # CartoDB::SQLApi
|
||||
119
services/sql-api/sql_api.rb
Normal file
119
services/sql-api/sql_api.rb
Normal file
@@ -0,0 +1,119 @@
|
||||
require 'open-uri'
|
||||
require 'json'
|
||||
require_relative '../../lib/carto/http/client'
|
||||
|
||||
module CartoDB
|
||||
class SQLApi
|
||||
|
||||
class SQLApiError < StandardError; end
|
||||
class SQLError < SQLApiError; end
|
||||
class PermissionError < SQLApiError; end
|
||||
class TimeoutError < SQLApiError; end
|
||||
class DnsError < SQLApiError; end
|
||||
|
||||
# seconds
|
||||
CONNECT_TIMEOUT = 45
|
||||
DEFAULT_TIMEOUT = 60
|
||||
|
||||
attr_accessor :api_key, :username, :timeout, :base_url
|
||||
attr_reader :response_code, :parsed_response
|
||||
|
||||
# privacy: 'public' / 'private'
|
||||
def self.with_username_api_key(username, api_key, privacy,
|
||||
base_url: ::ApplicationHelper.sql_api_url(username, privacy))
|
||||
new(
|
||||
base_url: base_url,
|
||||
username: username,
|
||||
api_key: api_key
|
||||
)
|
||||
end
|
||||
|
||||
def self.with_user(user, privacy)
|
||||
with_username_api_key(user.username, user.api_key, privacy)
|
||||
end
|
||||
|
||||
def initialize(arguments)
|
||||
self.username = arguments.fetch(:username)
|
||||
self.api_key = arguments.fetch(:api_key, nil)
|
||||
self.timeout = arguments.fetch(:timeout, DEFAULT_TIMEOUT)
|
||||
self.base_url = arguments.fetch(:base_url, nil)
|
||||
end
|
||||
|
||||
def url(query, format = '', filename = '')
|
||||
build_request(query, format, filename, :get, :public).url
|
||||
end
|
||||
|
||||
def export_table_url(table, format = 'gpkg', filename = table)
|
||||
query = %{select * from "#{table}"}
|
||||
url(query, format, filename)
|
||||
end
|
||||
|
||||
def fetch(query, format = '')
|
||||
request = build_request(query, format)
|
||||
response = request.run
|
||||
handle_response(response)
|
||||
end
|
||||
|
||||
def handle_response(response)
|
||||
@response_code = response.response_code
|
||||
body = inflate(response.body.to_s)
|
||||
@parsed_response = ::JSON.parse(body) rescue nil
|
||||
raise_if_error(response)
|
||||
parsed_response["rows"] rescue body
|
||||
end
|
||||
|
||||
def inflate(text)
|
||||
Zlib::GzipReader.new(StringIO.new(text)).read
|
||||
rescue Zlib::GzipFile::Error
|
||||
return text
|
||||
end
|
||||
|
||||
def raise_if_error(response)
|
||||
error_message = parsed_response["error"].first rescue nil
|
||||
raise DnsError if response.return_code == :couldnt_resolve_host
|
||||
raise TimeoutError if response.timed_out?
|
||||
raise PermissionError if error_message =~ /^permission denied for relation/
|
||||
raise SQLError.new(error_message) if response_code != 200
|
||||
end
|
||||
|
||||
def build_base_url(sql_api_config_type)
|
||||
config = ::Cartodb.config[:sql_api][sql_api_config_type.to_s]
|
||||
if self.base_url.nil?
|
||||
%Q[#{config["protocol"]}://#{username}.#{config["domain"]}#{config["endpoint"]}]
|
||||
else
|
||||
%Q[#{self.base_url}#{config["endpoint"]}]
|
||||
end
|
||||
end
|
||||
|
||||
def build_request(query, format = '', filename = '', method = :post, config_type = :private)
|
||||
params = build_params(query, format, filename)
|
||||
http_client = Carto::Http::Client.get('sql_api')
|
||||
request = http_client.request(
|
||||
build_base_url(config_type),
|
||||
method: method,
|
||||
headers: { 'Accept-Encoding' => 'gzip,deflate' },
|
||||
connecttimeout: CONNECT_TIMEOUT,
|
||||
timeout: timeout
|
||||
)
|
||||
set_request_parameters(request, method, params)
|
||||
end
|
||||
|
||||
def build_params(query, format = '', filename = '')
|
||||
params = {q: query}
|
||||
params["format"] = format if !format.empty?
|
||||
params["filename"] = filename if !filename.empty?
|
||||
params["api_key"] = api_key if !api_key.nil? && !api_key.empty?
|
||||
params
|
||||
end
|
||||
|
||||
def set_request_parameters(request, method, params)
|
||||
if method == :get
|
||||
request.options[:params] = URI.encode_www_form(params)
|
||||
else
|
||||
request.options[:body] = URI.encode_www_form(params)
|
||||
end
|
||||
request
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user