require "shopify_cli" module ShopifyCli class API include SmartProperties property! :ctx, accepts: ShopifyCli::Context property! :token, accepts: String property :auth_header, accepts: String property! :url, accepts: String class APIRequestError < StandardError attr_reader :response def initialize(message = nil, response: nil) super(message) @response = response end end class APIRequestNotFoundError < APIRequestError; end class APIRequestClientError < APIRequestError; end class APIRequestUnauthorizedError < APIRequestClientError; end class APIRequestForbiddenError < APIRequestClientError; end class APIRequestUnexpectedError < APIRequestError; end class APIRequestRetriableError < APIRequestError; end class APIRequestTimeoutError < APIRequestRetriableError; end class APIRequestServerError < APIRequestRetriableError; end class APIRequestThrottledError < APIRequestRetriableError; end def self.gid_to_id(gid) gid.split("/").last end def query(query_name, variables: {}) _, resp = request( body: JSON.dump(query: load_query(query_name).tr("\n", ""), variables: variables), url: url, ) ctx.debug(resp) resp rescue API::APIRequestServerError, API::APIRequestUnexpectedError => e ctx.puts(ctx.message("core.api.error.internal_server_error")) ctx.debug(ctx.message("core.api.error.internal_server_error_debug", e.message)) end def request(url:, body: nil, headers: {}, method: "POST") CLI::Kit::Util.begin do uri = URI.parse(url) unless uri.is_a?(URI::HTTP) ctx.abort(ctx.message("core.api.error.invalid_url", url)) end # we delay this require so as to avoid a performance hit on starting the CLI require "shopify-cli/http_request" headers = default_headers.merge(headers) response = if method == "POST" HttpRequest.post(uri, body, headers) elsif method == "PUT" HttpRequest.put(uri, body, headers) elsif method == "GET" HttpRequest.get(uri, body, headers) elsif method == "DELETE" HttpRequest.delete(uri, body, headers) end case response.code.to_i when 200..399 [response.code.to_i, JSON.parse(response.body), response] when 401 raise APIRequestUnauthorizedError.new("#{response.code}\n#{response.body}", response: response) when 403 raise APIRequestForbiddenError.new("#{response.code}\n#{response.body}", response: response) when 404 raise APIRequestNotFoundError.new("#{response.code}\n#{response.body}", response: response) when 429 raise APIRequestThrottledError.new("#{response.code}\n#{response.body}", response: response) when 400..499 raise APIRequestClientError.new("#{response.code}\n#{response.body}", response: response) when 500..599 raise APIRequestServerError.new("#{response.code}\n#{response.body}", response: response) else raise APIRequestUnexpectedError.new("#{response.code}\n#{response.body}", response: response) end rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ETIMEDOUT, Timeout::Error raise APIRequestTimeoutError.new("Timeout") end.retry_after(APIRequestRetriableError, retries: 3) do |e| sleep(1) if e.is_a?(APIRequestThrottledError) end end protected def load_query(name) project_type = ShopifyCli::Project.current_project_type project_file_path = File.join( ShopifyCli::ROOT, "lib", "project_types", project_type.to_s, "graphql", "#{name}.graphql" ) if !project_type.nil? && File.exist?(project_file_path) File.read(project_file_path) else File.read(File.join(ShopifyCli::ROOT, "lib", "graphql", "#{name}.graphql")) end end private def default_headers { "User-Agent" => "Shopify CLI; v=#{ShopifyCli::VERSION}", "Sec-CH-UA" => "Shopify CLI; v=#{ShopifyCli::VERSION} sha=#{ShopifyCli.sha}", "Sec-CH-UA-PLATFORM" => ctx.os, }.tap do |headers| headers["X-Shopify-Cli-Employee"] = "1" if Shopifolk.acting_as_shopify_organization? end.merge(auth_headers(token)) end def auth_headers(token) raise NotImplementedError if auth_header.nil? { auth_header => token } end end end