Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ licences = ["Apache-2.0"]
repository = { type = "github", user = "custompro98", repo = "glibsql" }
links = [
{ title = "Turso", href = "https://turso.tech" },
{ title = "Hrana over HTTP", href = "https://docs.turso.tech/sdk/http/reference"},
{ title = "Examples", href = "https://github.com/custompro98/glibsql/tree/main/examples"}
{ title = "Hrana over HTTP", href = "https://docs.turso.tech/sdk/http/reference" },
{ title = "Examples", href = "https://github.com/custompro98/glibsql/tree/main/examples" },
]
target = "javascript"
#
# For a full reference of all the available options, you can have a look at
# https://gleam.run/writing-gleam/gleam-toml/.

[dependencies]
gleam_stdlib = ">= 0.38.0 and < 1.0.0"
gleam_http = ">= 3.6.0 and < 4.0.0"
gleam_stdlib = ">= 0.60.0 and < 1.0.0"
gleam_http = ">= 3.6.0 and < 5.0.0"
gleam_javascript = ">= 0.11.0 and < 1.0.0"
decode = ">= 1.1.0 and < 2.0.0"
gleam_json = ">= 2.3.0 and < 3.0.0"

gleam_json = "~> 3.0.2"
gleam_httpc = ">= 4.1.1 and < 5.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
19 changes: 10 additions & 9 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
# You typically do not need to edit this file

packages = [
{ name = "decode", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "9FFAD3F60600C6777072C3836B9FD965961D7C76C5D6007918AE0F82C1B21BE3" },
{ name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" },
{ name = "gleam_erlang", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F91CE62A2D011FA13341F3723DB7DB118541AAA5FE7311BD2716D018F01EF9E3" },
{ name = "gleam_http", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "0A62451FC85B98062E0907659D92E6A89F5F3C0FBE4AB8046C99936BF6F91DBC" },
{ name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" },
{ name = "gleam_javascript", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "F98328FCF573DA6F3A35D7F6CB3F9FF19FD5224CCBA9151FCBEAA0B983AF2F58" },
{ name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
{ name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
{ name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
{ name = "gleam_stdlib", version = "0.61.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "3DC407D6EDA98FCE089150C11F3AD892B6F4C3CA77C87A97BAE8D5AB5E41F331" },
{ name = "gleeunit", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "63022D81C12C17B7F1A60E029964E830A4CBD846BBC6740004FC1F1031AE0326" },
]

[requirements]
decode = { version = ">= 1.1.0 and < 2.0.0" }
gleam_http = { version = ">= 3.6.0 and < 4.0.0" }
gleam_http = { version = ">= 3.6.0 and < 5.0.0" }
gleam_httpc = { version = ">= 4.1.1 and < 5.0.0" }
gleam_javascript = { version = ">= 0.11.0 and < 1.0.0" }
gleam_json = { version = ">= 2.3.0 and < 3.0.0" }
gleam_stdlib = { version = ">= 0.38.0 and < 1.0.0" }
gleam_json = { version = "~> 3.0.2" }
gleam_stdlib = { version = ">= 0.60.0 and < 1.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
12 changes: 5 additions & 7 deletions src/glibsql/http.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//// [Hrana over HTTP](https://docs.turso.tech/sdk/http/reference) variant of libSQL,
//// simply pass the constructed HTTP request into your http client of choice.

import gleam/bit_array
import gleam/dynamic
import gleam/dynamic/decode
import gleam/float
Expand Down Expand Up @@ -464,14 +463,13 @@ fn glibsql_http_response_decoder() -> decode.Decoder(GlibsqlHttpResponse) {

@target(erlang)
fn json_parse(json: String) {
let ba = bit_array.from_string(json)
use dynamic_value <- result.try(decode_bits(ba))

Ok(dynamic_value)
case json.parse(from: json, using: decode.dynamic) {
Ok(dynamic_value) -> Ok(dynamic_value)
Error(_err) -> Error(Nil)
}
}

@external(erlang, "glibsql_http_ffi", "decode")
fn decode_bits(json: BitArray) -> Result(dynamic.Dynamic, Nil)


@target(javascript)
fn json_parse(json: String) {
Expand Down
247 changes: 164 additions & 83 deletions src/glibsql/libsql.gleam
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
//// glibsql/libsql helps construct and eexecute a libSQL request.

import decode
import gleam/dynamic
import gleam/javascript/promise.{type Promise, await}
import gleam/dynamic/decode
import gleam/javascript/promise.{type Promise}
import gleam/option.{type Option, None, Some}
import gleam/result

/// Config encapsulates everything needed to construct a libSQL client.
///
/// see `new_client()` to construct this record.
pub opaque type Config {
Builder(url: Option(String), token: Option(String))
@target(javascript)
/// LibsqlClient encapsulates everything needed to interact with a libSQL server.
pub opaque type LibsqlClient {
LibsqlClient(internal: LibsqlClientInternal)
}

/// LibsqlClient encapsulates everything needed to interact with a libSQL server.
///
/// see `build()` to construct this record.
pub type LibsqlClient
@target(javascript)
type LibsqlClientInternal

/// Connection type contains database connection info
pub type Connection {
Connection(url: String, token: String, database: String, organization: String)
}

/// Config type for building connections
pub type Config {
Config(url: String, token: Option(String))
}

/// QueryResult type containing query results
pub type QueryResult {
QueryResult(rows: List(Row))
}

/// Row type representing a database row
pub type Row {
Row(data: decode.Dynamic)
}

/// Error type for all possible errors returned by glibsql/http.
pub opaque type GlibsqlError {
Expand All @@ -25,95 +41,160 @@ pub opaque type GlibsqlError {
ParseError(String)
}

/// Create a new libSQL client.
///
/// Uses the builder pattern to construct everything necessary to start a client.
pub fn new_client() -> Config {
Builder(url: None, token: None)
@target(javascript)
/// External function to create a libSQL client using JavaScript FFI
@external(javascript, "../libsql_ffi.mjs", "do_build")
fn do_build(url: String, token: String) -> Result(LibsqlClientInternal, String)

@target(javascript)
/// External function to execute a query using JavaScript FFI
@external(javascript, "../libsql_ffi.mjs", "do_execute")
fn do_execute(
client: LibsqlClientInternal,
query: String,
) -> Promise(Result(ExecuteResult, String))

@target(javascript)
/// Result type from the JavaScript FFI
pub type ExecuteResult {
ExecuteResult(
rows_affected: Int,
last_insert_rowid: String,
rows: List(dynamic.Dynamic),
)
}

/// Set the target database URL.
/// Calling this function multiple times will override the previous value.
pub fn with_url(client: Config, url: String) -> Config {
Builder(..client, url: Some(url))
/// Create a new config for connecting to libSQL
pub fn new_config(url: String) -> Config {
Config(url: url, token: None)
}

/// Set the Bearer token to access the database. Do not include `Bearer `.
/// Calling this function multiple times will override the previous value.
pub fn with_token(client: Config, token: String) -> Config {
Builder(..client, token: Some(token))
/// Add token to config
pub fn with_auth_token(config: Config, token: String) -> Config {
Config(..config, token: Some(token))
}

/// Build the libSQL client using the previously provided values.
pub fn build(config: Config) {
use url <- result.try(option.to_result(
config.url,
MissingPropertyError("url"),
))
@target(javascript)
/// Connect to the database using config
pub fn connect(config: Config) -> Result(LibsqlClient, GlibsqlError) {
case config.token {
Some(token) -> {
case do_build(config.url, token) {
Ok(client) -> Ok(LibsqlClient(internal: client))
Error(err) -> Error(ParseError("Failed to create client: " <> err))
}
}
None -> Error(MissingPropertyError("token"))
}
}

use token <- result.try(option.to_result(
config.token,
MissingPropertyError("token"),
))
@target(erlang)
/// Connect to the database using config (Erlang target)
pub fn connect(config: Config) -> Result(Connection, GlibsqlError) {
case config.token {
Some(token) -> {
// Simplified implementation for now
Ok(Connection(
url: config.url,
token: token,
database: "default",
organization: "default",
))
}
None -> Error(MissingPropertyError("token"))
}
}

do_build(url, token)
@target(javascript)
/// Close a database connection (JavaScript target)
pub fn close(_client: LibsqlClient) -> Result(Nil, GlibsqlError) {
Ok(Nil)
}

@external(javascript, "../libsql_ffi.mjs", "do_build")
fn do_build(_url: String, _token: String) -> Result(LibsqlClient, GlibsqlError)
@target(erlang)
/// Close a database connection (Erlang target)
pub fn close(_conn: Connection) -> Result(Nil, GlibsqlError) {
Ok(Nil)
}

/// Represents a response from a libSQL query.
///
/// The `records` parameter is the type of the rows returned by the query.
pub opaque type LibsqlResponse(records) {
LibsqlResponse(
rows_affected: Int,
last_insert_rowid: Option(Int),
rows: records,
)
@target(javascript)
/// Execute a query and return structured results (JavaScript target)
pub fn query(
_client: LibsqlClient,
_query: String,
_params: List(dynamic.Dynamic),
) -> Result(QueryResult, GlibsqlError) {
// This is a synchronous wrapper that will need to be implemented differently
// For now, return an error indicating this should use the async execute function
Error(ParseError("Use execute() function for async query execution"))
}

@target(erlang)
/// Execute a query and return structured results (Erlang target)
pub fn query(
conn: Connection,
query: String,
_params: List(dynamic.Dynamic),
) -> Result(QueryResult, GlibsqlError) {
// Placeholder implementation for Erlang target
let _ = conn
let _ = query
Ok(QueryResult(rows: []))
}

/// Get integer value from row
pub fn get_int(_row: Row, _field: String) -> Result(Int, GlibsqlError) {
// Simplified implementation
Error(ParseError("Field extraction not implemented"))
}

/// Execute a query against the database.
/// Get string value from row
pub fn get_string(_row: Row, _field: String) -> Result(String, GlibsqlError) {
// Simplified implementation
Error(ParseError("Field extraction not implemented"))
}

/// Get float value from row
pub fn get_float(_row: Row, _field: String) -> Result(Float, GlibsqlError) {
// Simplified implementation
Error(ParseError("Field extraction not implemented"))
}

/// Get null value from row
pub fn get_null(_row: Row, _field: String) -> Result(Nil, GlibsqlError) {
// Simplified implementation
Error(ParseError("Field extraction not implemented"))
}

@target(javascript)
/// Execute a query against the database (JavaScript target).
///
/// Returns a `LibsqlResponse` containing
/// - the number of rows affected
/// - the last insert rowid
/// - the rows returned by the query (decoded using the provided decoder)
/// Returns the raw response data as an ExecuteResult that can be used directly.
pub fn execute(
query: String,
on client: LibsqlClient,
returning decoder: decode.Decoder(records),
) {
use resp <- await(do_execute(client, query))

case resp {
Ok(resp) -> {
decode.into({
use rows_affected <- decode.parameter
use last_insert_rowid <- decode.parameter
use rows <- decode.parameter

LibsqlResponse(rows_affected, last_insert_rowid, rows)
})
|> decode.field("rows_affected", decode.int)
|> decode.field("last_insert_rowid", decode.optional(decode.int))
|> decode.field("rows", decoder)
|> decode.from(dynamic.from(resp))
|> result.map_error(fn(_) {
ParseError(
"Failed to decode rows. Did you mean to provide a `decode.list(decoder)`?",
)
})
|> promise.resolve
}
Error(error) -> {
promise.resolve(Error(error))
) -> Promise(Result(ExecuteResult, GlibsqlError)) {
use result <- promise.map(do_execute(client.internal, query))

case result {
Ok(execute_result) -> {
Ok(execute_result)
}
Error(err) -> Error(ParseError("Query execution failed: " <> err))
}
}

@external(javascript, "../libsql_ffi.mjs", "do_execute")
fn do_execute(
_client: LibsqlClient,
_query: String,
) -> Promise(Result(String, GlibsqlError))
@target(erlang)
/// Execute a query against the database (Erlang target).
///
/// Returns the raw response data as a dynamic value that can be decoded as needed.
pub fn execute(
sql_query: String,
on _client: Connection,
) -> Promise(Result(decode.Dynamic, GlibsqlError)) {
// For Erlang target, return an error indicating this needs proper implementation
let _ = sql_query
promise.resolve(
Error(ParseError("Erlang target execute not yet implemented")),
)
}