libs/SQL.bend source
libs/SQL.bend on the hub · documented module
import Baseimport ./HTTPClient.bend as HttpClientimport ./JSON.bend as Json# Datasette's JSON API over the HTTP client. The base URL must not end in '/'.type SQL.Client is Data: Client{base_url: String, database: String, api_token: String}type SQL.Error is Data: Transport{error: HttpClient.Error} InvalidJson{status: U32} InvalidParameters{} HttpStatus{status: U32, body: Json.Value}def hex_digit_kind(n: U32, decimal: Bool) -> U32: match decimal: case True{}: (48 + n : U32) case False{}: (55 + n : U32)def hex_digit(+n: U32) -> U32: hex_digit_kind(n, U32.is_lt(n, 10))def percent_byte(+byte: U32) -> String: SCon{Chr{37}, SCon{Chr{hex_digit(U32.div(byte, 16))}, SCon{Chr{hex_digit(U32.mod(byte, 16))}, SNil{}}}}def percent_bytes(bytes: List<&2, U32>) -> String: match bytes: case Nil{}: "" case head <> tail: percent_byte(head) ++ percent_bytes(tail)def percent_component(text: String) -> String: percent_bytes(HttpClient.utf8(text))def query_suffix(query: String, has_query: Bool) -> String: match has_query: case True{}: "?" ++ query case False{}: ""def datasette_url(base_url: String, database: String, path: String, query: String, has_query: Bool) -> String: base_url ++ "/" ++ percent_component(database) ++ path ++ query_suffix(query, has_query)def sql_header(name: String, value: String) -> HttpClient.Header: HttpClient.Header{name, HttpClient.utf8(value)}def unauthenticated_headers(json_body: Bool) -> List<&2, HttpClient.Header>: match json_body: case True{}: [sql_header("Accept", "application/json"), sql_header("Content-Type", "application/json")] case False{}: [sql_header("Accept", "application/json")]def authenticated_headers(+api_token: String, json_body: Bool) -> List<&2, HttpClient.Header>: match json_body: case True{}: [sql_header("Accept", "application/json"), sql_header("Content-Type", "application/json"), sql_header("Authorization", "Bearer " ++ api_token)] case False{}: [sql_header("Accept", "application/json"), sql_header("Authorization", "Bearer " ++ api_token)]def datasette_headers_kind(+api_token: String, json_body: Bool, empty_token: Bool) -> List<&2, HttpClient.Header>: match empty_token: case True{}: unauthenticated_headers(json_body) case False{}: authenticated_headers(api_token, json_body)def datasette_headers(+api_token: String, json_body: Bool) -> List<&2, HttpClient.Header>: datasette_headers_kind(api_token, json_body, String.is_empty(api_token))def parameter_bool(value: Bool) -> String: match value: case True{}: "1" case False{}: "0"def parameter_value(value: Json.Value) -> String: match value: case Json.Str{text}: text case Json.Bool{value}: parameter_bool(value) case Json.Null{}: "" case _: Json.stringify(value)def encode_parameters(fields: List<&2, Sigma<&2, &2, String, _ => Json.Value>>) -> String: match fields: case Nil{}: "" case (name, value) <> tail: "&" ++ percent_component(name) ++ "=" ++ percent_component(parameter_value(value)) ++ encode_parameters(tail)def query_request(client: SQL.Client, sql: String) -> HttpClient.Request: Client{base_url, database, api_token} = client HttpClient.Request{ "GET", datasette_url(base_url, database, "/-/query.json", "sql=" ++ percent_component(sql), True{}), datasette_headers(api_token, False{}), Nil{}}def query_request_with_params(client: SQL.Client, sql: String, params: Json.Value) -> Result<&1, &1, SQL.Error, HttpClient.Request>: Client{base_url, database, api_token} = client match params: case Json.Obj{fields}: Done{HttpClient.Request{ "GET", datasette_url(base_url, database, "/-/query.json", "sql=" ++ percent_component(sql) ++ encode_parameters(fields), True{}), datasette_headers(api_token, False{}), Nil{}}} case _: Fail{InvalidParameters{}}def write_body(sql: String, params: Json.Value) -> String: Json.stringify(Json.Obj{ ("sql", Json.Str{sql}) <> ("params", params) <> Nil{}})def execute_write_request(client: SQL.Client, sql: String, params: Json.Value) -> HttpClient.Request: Client{base_url, database, api_token} = client HttpClient.Request{ "POST", datasette_url(base_url, database, "/-/execute-write", "", False{}), datasette_headers(api_token, True{}), HttpClient.utf8(write_body(sql, params))}def datasette_status_ok(+status: U32) -> Bool: Bool.and(U32.is_le(200, status), U32.is_lt(status, 300))def parsed_value_response(status: U32, value: Json.Value, success: Bool) -> Result<&1, &1, SQL.Error, Json.Value>: match success: case True{}: Done{value} case False{}: Fail{HttpStatus{status, value}}def parsed_response_kind(status: U32, parsed: Result<&1, &1, HttpClient.Error, Json.Value>, success: Bool) -> Result<&1, &1, SQL.Error, Json.Value>: match parsed: case Fail{_}: Fail{InvalidJson{status}} case Done{value}: parsed_value_response(status, value, success)def parsed_response(+status: U32, parsed: Result<&1, &1, HttpClient.Error, Json.Value>) -> Result<&1, &1, SQL.Error, Json.Value>: parsed_response_kind(status, parsed, datasette_status_ok(status))def response_json(response: HttpClient.Response) -> Result<&1, &1, SQL.Error, Json.Value>: HttpClient.Response{+status, headers, body, trailers} = response parsed_response(status, HttpClient.read_json(response))def response_result(result: Result<&1, &1, HttpClient.Error, HttpClient.Response>) -> Result<&1, &1, SQL.Error, Json.Value>: match result: case Fail{error}: Fail{Transport{error}} case Done{response}: response_json(response)def query(+client: SQL.Client, +sql: String, +options: HttpClient.Options) -> IO(Result<&1, &1, SQL.Error, Json.Value>): do IO<Result<&1, &1, SQL.Error, Json.Value>>: response : Result<&1, &1, HttpClient.Error, HttpClient.Response> <- HttpClient.send(query_request(client, sql), options) return response_result(response)def query_with_params_io(request: Result<&1, &1, SQL.Error, HttpClient.Request>, +options: HttpClient.Options) -> IO(Result<&1, &1, SQL.Error, Json.Value>): match request: case Fail{error}: IO.pure(Result<&1, &1, SQL.Error, Json.Value>, Fail{error}) case Done{value}: do IO<Result<&1, &1, SQL.Error, Json.Value>>: response : Result<&1, &1, HttpClient.Error, HttpClient.Response> <- HttpClient.send(value, options) return response_result(response)def query_with_params(+client: SQL.Client, +sql: String, +params: Json.Value, +options: HttpClient.Options) -> IO(Result<&1, &1, SQL.Error, Json.Value>): query_with_params_io(query_request_with_params(client, sql, params), options)def execute_write(+client: SQL.Client, +sql: String, +params: Json.Value, +options: HttpClient.Options) -> IO(Result<&1, &1, SQL.Error, Json.Value>): do IO<Result<&1, &1, SQL.Error, Json.Value>>: response : Result<&1, &1, HttpClient.Error, HttpClient.Response> <- HttpClient.send(execute_write_request(client, sql, params), options) return response_result(response)