diff --git a/README.md b/README.md index 9e9add8..67e0045 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,11 @@ pub fn main() { base_request() |> glibsql.with_statement(glibsql.ExecuteStatement( sql: "BEGIN", + arguments: None, )) |> glibsql.with_statement(glibsql.ExecuteStatement( sql: "SELECT * FROM users", + arguments: None, )) |> glibsql.build diff --git a/examples/00-http-select/src/app.gleam b/examples/00-http-select/src/app.gleam index 2e14083..2246c09 100644 --- a/examples/00-http-select/src/app.gleam +++ b/examples/00-http-select/src/app.gleam @@ -23,14 +23,16 @@ pub fn main() { } } + let statement = + glibsql.new_statement() + |> glibsql.with_query("SELECT id, email, created_at, updated_at FROM users") + let request = glibsql.new_request() |> glibsql.with_database(env.database_name) |> glibsql.with_organization(env.database_organization) |> glibsql.with_token(env.database_auth_token) - |> glibsql.with_statement(glibsql.ExecuteStatement( - sql: "SELECT id, email, created_at, updated_at FROM users", - )) + |> glibsql.with_statement(statement) |> glibsql.with_statement(glibsql.CloseStatement) |> glibsql.build diff --git a/examples/01-http-select-anonymous-arguments/.gitignore b/examples/01-http-select-anonymous-arguments/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/examples/01-http-select-anonymous-arguments/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/examples/01-http-select-anonymous-arguments/README.md b/examples/01-http-select-anonymous-arguments/README.md new file mode 100644 index 0000000..02b0bef --- /dev/null +++ b/examples/01-http-select-anonymous-arguments/README.md @@ -0,0 +1,24 @@ +# app + +[![Package Version](https://img.shields.io/hexpm/v/app)](https://hex.pm/packages/app) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/app/) + +```sh +gleam add app@1 +``` +```gleam +import app + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/examples/01-http-select-anonymous-arguments/gleam.toml b/examples/01-http-select-anonymous-arguments/gleam.toml new file mode 100644 index 0000000..6e0c88a --- /dev/null +++ b/examples/01-http-select-anonymous-arguments/gleam.toml @@ -0,0 +1,27 @@ +name = "app" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# 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.34.0 and < 2.0.0" +glenv = ">= 0.4.0 and < 1.0.0" +gleam_httpc = ">= 2.2.0 and < 3.0.0" +# glibsql = ">= 0.5.1 and < 1.0.0" +glibsql = { path = "../../" } +decode = ">= 0.2.0 and < 1.0.0" +dot_env = ">= 1.0.0 and < 2.0.0" +thoas = ">= 1.2.1 and < 2.0.0" +birl = ">= 1.7.1 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/examples/01-http-select-anonymous-arguments/manifest.toml b/examples/01-http-select-anonymous-arguments/manifest.toml new file mode 100644 index 0000000..93e2a73 --- /dev/null +++ b/examples/01-http-select-anonymous-arguments/manifest.toml @@ -0,0 +1,32 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "decode", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "965F517F67B8C172CA27A5C8E34C73733139E8C9E64736181B8C3179281F9793" }, + { name = "dot_env", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "E7B84DC7B579553AF3B9F0A03B2F8DDB9B44521F553CCFBE633AA595C27F1A05" }, + { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, + { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, + { name = "gleam_javascript", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "483631D3001FCE8EB12ADEAD5E1B808440038E96F93DA7A32D326C82F480C0B2" }, + { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, + { name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glenv", version = "0.4.0", build_tools = ["gleam"], requirements = ["decode", "envoy", "gleam_stdlib"], otp_app = "glenv", source = "hex", outer_checksum = "46164B9FFEB08927FD2CEBD96C3AFBE082AD5CC2C6F2FC4A78EFDA9EB61E3510" }, + { name = "glibsql", version = "0.5.1", build_tools = ["gleam"], requirements = ["decode", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib"], source = "local", path = "../.." }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "simplifile", version = "2.0.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "5FFEBD0CAB39BDD343C3E1CCA6438B2848847DC170BA2386DF9D7064F34DF000" }, + { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, +] + +[requirements] +birl = { version = ">= 1.7.1 and < 2.0.0"} +decode = { version = ">= 0.2.0 and < 1.0.0" } +dot_env = { version = ">= 1.0.0 and < 2.0.0" } +gleam_httpc = { version = ">= 2.2.0 and < 3.0.0" } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +glenv = { version = ">= 0.4.0 and < 1.0.0" } +glibsql = { path = "../../" } +thoas = { version = ">= 1.2.1 and < 2.0.0" } diff --git a/examples/01-http-select-anonymous-arguments/src/app.gleam b/examples/01-http-select-anonymous-arguments/src/app.gleam new file mode 100644 index 0000000..4b7e304 --- /dev/null +++ b/examples/01-http-select-anonymous-arguments/src/app.gleam @@ -0,0 +1,98 @@ +import app/internal/env +import birl +import gleam/httpc +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/result +import glibsql/http as glibsql + +pub type User { + User( + id: Int, + email: String, + created_at: birl.Time, + updated_at: Option(birl.Time), + ) +} + +pub fn main() { + let env = case env.load() { + Ok(env) -> env + Error(reason) -> { + panic as reason + } + } + + let statement = + glibsql.new_statement() + |> glibsql.with_query( + "SELECT id, email, created_at, updated_at FROM users WHERE id = ?", + ) + |> glibsql.with_argument(glibsql.AnonymousArgument(glibsql.Integer(1))) + + let request = + glibsql.new_request() + |> glibsql.with_database(env.database_name) + |> glibsql.with_organization(env.database_organization) + |> glibsql.with_token(env.database_auth_token) + |> glibsql.with_statement(statement) + |> glibsql.with_statement(glibsql.CloseStatement) + |> glibsql.build + + let assert Ok(request) = request + + use response <- result.try(httpc.send(request)) + + let users = + glibsql.decode_response(response.body) + |> result.map(fn(resp) { + list.filter(resp.results, fn(res) { + case res { + glibsql.ExecuteResponse(_, _) -> True + glibsql.CloseResponse -> False + } + }) + |> list.flat_map(fn(res) { + case res { + glibsql.ExecuteResponse(_columns, rows) -> { + list.map(rows, fn(row) { + let assert [id, email, created_at, updated_at] = row.values + + User( + id: case id { + glibsql.Integer(value) -> value + _ -> panic as "Unexpected type" + }, + email: case email { + glibsql.Text(value) -> value + _ -> panic as "Unexpected type" + }, + created_at: case created_at { + glibsql.Datetime(value) -> to_time(value) + _ -> panic as "Unexpected type" + }, + updated_at: case updated_at { + glibsql.Datetime(value) -> Some(to_time(value)) + glibsql.Null -> None + _ -> panic as "Unexpected type" + }, + ) + }) + } + _ -> [] + } + }) + }) + |> result.unwrap([]) + + let assert [ + User(1, "joe@example.com", _user_1_created_at, None), + ] = users + + Ok(Nil) +} + +fn to_time(value: String) -> birl.Time { + let assert Ok(time) = birl.parse(value) + time +} diff --git a/examples/01-http-select-anonymous-arguments/src/app/internal/env.gleam b/examples/01-http-select-anonymous-arguments/src/app/internal/env.gleam new file mode 100644 index 0000000..926c887 --- /dev/null +++ b/examples/01-http-select-anonymous-arguments/src/app/internal/env.gleam @@ -0,0 +1,60 @@ +import decode +import dot_env +import gleam/list +import gleam/string +import glenv + +pub type Env { + Env( + database_name: String, + database_organization: String, + database_auth_token: String, + ) +} + +const definitions = [ + #("DATABASE_NAME", glenv.String), #("DATABASE_ORGANIZATION", glenv.String), + #("DATABASE_AUTH_TOKEN", glenv.String), +] + +pub fn load() -> Result(Env, String) { + dot_env.load_default() + + let decoder = + decode.into({ + use database_name <- decode.parameter + use database_organization <- decode.parameter + use database_auth_token <- decode.parameter + + Env(database_name, database_organization, database_auth_token) + }) + |> decode.field("DATABASE_NAME", decode.string) + |> decode.field("DATABASE_ORGANIZATION", decode.string) + |> decode.field("DATABASE_AUTH_TOKEN", decode.string) + + case glenv.load(decoder, definitions) { + Ok(env) -> Ok(env) + Error(err) -> { + let reason = case err { + glenv.NotFoundError(key) -> "Environment variable not found: " <> key + glenv.ParseError(key, _) -> + "Failed to parse environment variable: " <> key + glenv.DefinitionMismatchError(errors) -> { + let errors = + list.map(errors, fn(error) { + error.expected + <> " expected, got " + <> error.found + <> " at " + <> string.join(error.path, "->") + }) + + "Failed to match environment definition: " + <> string.join(errors, ", ") + } + } + + Error(reason) + } + } +} diff --git a/examples/01-http-select-anonymous-arguments/src/app_ffi.erl b/examples/01-http-select-anonymous-arguments/src/app_ffi.erl new file mode 100644 index 0000000..21d15c6 --- /dev/null +++ b/examples/01-http-select-anonymous-arguments/src/app_ffi.erl @@ -0,0 +1,8 @@ +-module(app_ffi). + +-export([ + decode/1 +]). + +decode(Json) -> + thoas:decode(Json). diff --git a/examples/01-http-select-anonymous-arguments/test/app_test.gleam b/examples/01-http-select-anonymous-arguments/test/app_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/examples/01-http-select-anonymous-arguments/test/app_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/examples/02-http-select-named-arguments/.gitignore b/examples/02-http-select-named-arguments/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/examples/02-http-select-named-arguments/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/examples/02-http-select-named-arguments/README.md b/examples/02-http-select-named-arguments/README.md new file mode 100644 index 0000000..02b0bef --- /dev/null +++ b/examples/02-http-select-named-arguments/README.md @@ -0,0 +1,24 @@ +# app + +[![Package Version](https://img.shields.io/hexpm/v/app)](https://hex.pm/packages/app) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/app/) + +```sh +gleam add app@1 +``` +```gleam +import app + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/examples/02-http-select-named-arguments/gleam.toml b/examples/02-http-select-named-arguments/gleam.toml new file mode 100644 index 0000000..6e0c88a --- /dev/null +++ b/examples/02-http-select-named-arguments/gleam.toml @@ -0,0 +1,27 @@ +name = "app" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# 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.34.0 and < 2.0.0" +glenv = ">= 0.4.0 and < 1.0.0" +gleam_httpc = ">= 2.2.0 and < 3.0.0" +# glibsql = ">= 0.5.1 and < 1.0.0" +glibsql = { path = "../../" } +decode = ">= 0.2.0 and < 1.0.0" +dot_env = ">= 1.0.0 and < 2.0.0" +thoas = ">= 1.2.1 and < 2.0.0" +birl = ">= 1.7.1 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/examples/02-http-select-named-arguments/manifest.toml b/examples/02-http-select-named-arguments/manifest.toml new file mode 100644 index 0000000..93e2a73 --- /dev/null +++ b/examples/02-http-select-named-arguments/manifest.toml @@ -0,0 +1,32 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "decode", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "965F517F67B8C172CA27A5C8E34C73733139E8C9E64736181B8C3179281F9793" }, + { name = "dot_env", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "E7B84DC7B579553AF3B9F0A03B2F8DDB9B44521F553CCFBE633AA595C27F1A05" }, + { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, + { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, + { name = "gleam_javascript", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "483631D3001FCE8EB12ADEAD5E1B808440038E96F93DA7A32D326C82F480C0B2" }, + { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, + { name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glenv", version = "0.4.0", build_tools = ["gleam"], requirements = ["decode", "envoy", "gleam_stdlib"], otp_app = "glenv", source = "hex", outer_checksum = "46164B9FFEB08927FD2CEBD96C3AFBE082AD5CC2C6F2FC4A78EFDA9EB61E3510" }, + { name = "glibsql", version = "0.5.1", build_tools = ["gleam"], requirements = ["decode", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib"], source = "local", path = "../.." }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "simplifile", version = "2.0.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "5FFEBD0CAB39BDD343C3E1CCA6438B2848847DC170BA2386DF9D7064F34DF000" }, + { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, +] + +[requirements] +birl = { version = ">= 1.7.1 and < 2.0.0"} +decode = { version = ">= 0.2.0 and < 1.0.0" } +dot_env = { version = ">= 1.0.0 and < 2.0.0" } +gleam_httpc = { version = ">= 2.2.0 and < 3.0.0" } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +glenv = { version = ">= 0.4.0 and < 1.0.0" } +glibsql = { path = "../../" } +thoas = { version = ">= 1.2.1 and < 2.0.0" } diff --git a/examples/02-http-select-named-arguments/src/app.gleam b/examples/02-http-select-named-arguments/src/app.gleam new file mode 100644 index 0000000..9069537 --- /dev/null +++ b/examples/02-http-select-named-arguments/src/app.gleam @@ -0,0 +1,106 @@ +import app/internal/env +import birl +import gleam/httpc +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/result +import glibsql/http as glibsql + +pub type User { + User( + id: Int, + email: String, + created_at: birl.Time, + updated_at: Option(birl.Time), + ) +} + +pub fn main() { + let env = case env.load() { + Ok(env) -> env + Error(reason) -> { + panic as reason + } + } + + let statement = + glibsql.new_statement() + |> glibsql.with_query( + "SELECT id, email, created_at, updated_at FROM users WHERE id = :id OR email = $email", + ) + |> glibsql.with_argument(glibsql.NamedArgument( + name: "email", + value: glibsql.Text("tom@example.com"), + )) + |> glibsql.with_argument(glibsql.NamedArgument( + name: "id", + value: glibsql.Integer(1), + )) + + let request = + glibsql.new_request() + |> glibsql.with_database(env.database_name) + |> glibsql.with_organization(env.database_organization) + |> glibsql.with_token(env.database_auth_token) + |> glibsql.with_statement(statement) + |> glibsql.with_statement(glibsql.CloseStatement) + |> glibsql.build + + let assert Ok(request) = request + + use response <- result.try(httpc.send(request)) + + let users = + glibsql.decode_response(response.body) + |> result.map(fn(resp) { + list.filter(resp.results, fn(res) { + case res { + glibsql.ExecuteResponse(_, _) -> True + glibsql.CloseResponse -> False + } + }) + |> list.flat_map(fn(res) { + case res { + glibsql.ExecuteResponse(_columns, rows) -> { + list.map(rows, fn(row) { + let assert [id, email, created_at, updated_at] = row.values + + User( + id: case id { + glibsql.Integer(value) -> value + _ -> panic as "Unexpected type" + }, + email: case email { + glibsql.Text(value) -> value + _ -> panic as "Unexpected type" + }, + created_at: case created_at { + glibsql.Datetime(value) -> to_time(value) + _ -> panic as "Unexpected type" + }, + updated_at: case updated_at { + glibsql.Datetime(value) -> Some(to_time(value)) + glibsql.Null -> None + _ -> panic as "Unexpected type" + }, + ) + }) + } + _ -> [] + } + }) + }) + |> result.unwrap([]) + + let assert [ + User(1, "joe@example.com", _user_1_created_at, None), + User(4, "tom@example.com", _user_4_created_at, None), + ] = users + + Ok(Nil) +} + +fn to_time(value: String) -> birl.Time { + let assert Ok(time) = birl.parse(value) + time +} diff --git a/examples/02-http-select-named-arguments/src/app/internal/env.gleam b/examples/02-http-select-named-arguments/src/app/internal/env.gleam new file mode 100644 index 0000000..926c887 --- /dev/null +++ b/examples/02-http-select-named-arguments/src/app/internal/env.gleam @@ -0,0 +1,60 @@ +import decode +import dot_env +import gleam/list +import gleam/string +import glenv + +pub type Env { + Env( + database_name: String, + database_organization: String, + database_auth_token: String, + ) +} + +const definitions = [ + #("DATABASE_NAME", glenv.String), #("DATABASE_ORGANIZATION", glenv.String), + #("DATABASE_AUTH_TOKEN", glenv.String), +] + +pub fn load() -> Result(Env, String) { + dot_env.load_default() + + let decoder = + decode.into({ + use database_name <- decode.parameter + use database_organization <- decode.parameter + use database_auth_token <- decode.parameter + + Env(database_name, database_organization, database_auth_token) + }) + |> decode.field("DATABASE_NAME", decode.string) + |> decode.field("DATABASE_ORGANIZATION", decode.string) + |> decode.field("DATABASE_AUTH_TOKEN", decode.string) + + case glenv.load(decoder, definitions) { + Ok(env) -> Ok(env) + Error(err) -> { + let reason = case err { + glenv.NotFoundError(key) -> "Environment variable not found: " <> key + glenv.ParseError(key, _) -> + "Failed to parse environment variable: " <> key + glenv.DefinitionMismatchError(errors) -> { + let errors = + list.map(errors, fn(error) { + error.expected + <> " expected, got " + <> error.found + <> " at " + <> string.join(error.path, "->") + }) + + "Failed to match environment definition: " + <> string.join(errors, ", ") + } + } + + Error(reason) + } + } +} diff --git a/examples/02-http-select-named-arguments/src/app_ffi.erl b/examples/02-http-select-named-arguments/src/app_ffi.erl new file mode 100644 index 0000000..21d15c6 --- /dev/null +++ b/examples/02-http-select-named-arguments/src/app_ffi.erl @@ -0,0 +1,8 @@ +-module(app_ffi). + +-export([ + decode/1 +]). + +decode(Json) -> + thoas:decode(Json). diff --git a/examples/02-http-select-named-arguments/test/app_test.gleam b/examples/02-http-select-named-arguments/test/app_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/examples/02-http-select-named-arguments/test/app_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +} diff --git a/gleam.toml b/gleam.toml index 9de23cc..9aa28a7 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "glibsql" -version = "0.6.0" +version = "0.7.0" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. @@ -9,7 +9,8 @@ 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 = "Hrana over HTTP", href = "https://docs.turso.tech/sdk/http/reference"}, + { title = "Examples", href = "https://github.com/custompro98/glibsql/tree/main/examples"} ] target = "javascript" # diff --git a/src/glibsql/http.gleam b/src/glibsql/http.gleam index cf189ba..e83fd3c 100644 --- a/src/glibsql/http.gleam +++ b/src/glibsql/http.gleam @@ -1,4 +1,5 @@ -//// glibsql/http helps construct a `gleam/http/request` for use with the [Hrana over HTTP](https://docs.turso.tech/sdk/http/reference) variant of libSQL, +//// glibsql/http helps construct a `gleam/http/request` for use with the +//// [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 decode @@ -16,16 +17,28 @@ import gleam/string // Request side +/// Arguments are the arguments to a query, either anonymous or named. +/// Only one of the types may be used per statement. +pub type Argument { + /// AnonymousArguments are the anonymous arguments to a query specified using the `?` syntax. + AnonymousArgument(value: Value) + /// NamedArguments are the named arguments to a query specified using either + /// the `:name`, the `@name`, or the `$name` syntax. + NamedArgument(name: String, value: Value) +} + /// Statement wraps the supported types of requests. /// A series of `ExecuteStatement(String)`s can be applied and /// be conditionally followed with a `CloseStatement` to close /// a connection when you are done with it. +/// +/// See `new_statement()` to construct this record. pub type Statement { /// `ExecuteStatement` contains a query that will be executed as written. /// There is no SQL-injection protection provided, this type of statement /// should be used with a query builder that can render the built query /// to a prepared string. - ExecuteStatement(sql: String) + ExecuteStatement(query: String, arguments: Option(List(Argument))) /// `CloseStatment` will either close the connection used in the current /// pipeline or will close the connection referenced by the request baton. /// Note: connections will be automatically closed by Turso after a 10s timeout. @@ -130,6 +143,49 @@ pub fn with_baton(request: HttpRequest, baton: String) -> HttpRequest { HttpRequest(..request, baton: Some(baton)) } +/// Create a new ExecuteStatement. +/// +/// Uses the builder pattern to construct everything necessary to send a request. +pub fn new_statement() -> Statement { + ExecuteStatement("", None) +} + +/// Set a query on the ExecuteStatement. +/// Calling this function multiple times will override the previous value. +pub fn with_query(statement: Statement, query: String) -> Statement { + case statement { + ExecuteStatement(_, arguments) -> { + ExecuteStatement(query, arguments) + } + CloseStatement -> CloseStatement + } +} + +/// Set an argument on the ExecuteStatement. +/// This function may be called multiple times, additional arguments will be +/// applied in order. +pub fn with_argument(statement: Statement, argument: Argument) -> Statement { + case statement { + ExecuteStatement(query, Some(arguments)) -> { + ExecuteStatement(query, Some([argument, ..arguments])) + } + ExecuteStatement(query, None) -> { + ExecuteStatement(query, Some([argument])) + } + CloseStatement -> CloseStatement + } +} + +/// Clear all arguments from the ExecuteStatement. +pub fn clear_arguments(statement: Statement) -> Statement { + case statement { + ExecuteStatement(query, _) -> { + ExecuteStatement(query, None) + } + CloseStatement -> CloseStatement + } +} + /// Build the request using the previously provided values. /// Returns a gleam/http request suitable to be used in your HTTP client of choice. pub fn build( @@ -165,7 +221,7 @@ pub fn build( ) |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.6.0") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") |> http_request.set_body(build_json(request)), ) } @@ -175,10 +231,17 @@ fn build_json(req: HttpRequest) { list.reverse(req.statements) |> list.map(fn(stmt) { case stmt { - ExecuteStatement(sql: sql) -> { + ExecuteStatement(query: query, arguments: arguments) -> { json.object([ #("type", json.string("execute")), - #("stmt", json.object([#("sql", json.string(sql))])), + #( + "stmt", + json.object([ + #("sql", json.string(query)), + #("args", build_anonymous_arguments(arguments)), + #("named_args", build_named_arguments(arguments)), + ]), + ), ]) } CloseStatement -> { @@ -194,23 +257,119 @@ fn build_json(req: HttpRequest) { |> json.to_string } -// IResponse side +fn build_anonymous_arguments(arguments: Option(List(Argument))) -> json.Json { + case arguments { + Some(arguments) -> { + arguments + |> list.filter(fn(arg) { + case arg { + AnonymousArgument(_) -> True + NamedArgument(_, _) -> False + } + }) + |> list.map(fn(arg) { + case arg { + AnonymousArgument(value) -> build_inner_argument_value(value) + + NamedArgument(_, _) -> { + panic as "Named arguments are not supported in anonymous arguments" + } + } + }) + |> json.preprocessed_array + } + None -> json.preprocessed_array([]) + } +} + +fn build_named_arguments(arguments: Option(List(Argument))) { + case arguments { + Some(arguments) -> { + arguments + |> list.filter(fn(arg) { + case arg { + NamedArgument(_, _) -> True + AnonymousArgument(_) -> False + } + }) + |> list.map(fn(arg) { + case arg { + NamedArgument(name, argument) -> { + json.object([ + #("name", json.string(name)), + #("value", build_inner_argument_value(argument)), + ]) + } + AnonymousArgument(_) -> { + panic as "Anonymous arguments are not supported in named arguments" + } + } + }) + |> json.preprocessed_array + } + None -> json.preprocessed_array([]) + } +} + +fn build_inner_argument_value(value: Value) -> json.Json { + case value { + Integer(value) -> + json.object([ + #("type", json.string("integer")), + #("value", json.string(int.to_string(value))), + ]) + Real(value) -> + json.object([ + #("type", json.string("float")), + #("value", json.string(float.to_string(value))), + ]) + Boolean(value) -> + json.object([ + #("type", json.string("integer")), + #( + "value", + json.string(case value { + True -> "1" + False -> "0" + }), + ), + ]) + Text(value) -> + json.object([ + #("type", json.string("text")), + #("value", json.string(value)), + ]) + Datetime(value) -> + json.object([ + #("type", json.string("text")), + #("value", json.string(value)), + ]) + Blob(value) -> + json.object([ + #("type", json.string("blob")), + #("base64", json.string(value)), + ]) + Null -> json.object([#("type", json.string("null"))]) + } +} + +// Response side -/// Values are the actual column values within a row. +/// Values are actual column values. pub type Value { - /// Integers are the integer values returned from a query. + /// Integers are integer values. Integer(value: Int) - /// Reals are the float values returned from a query. + /// Reals are float values. Real(value: Float) - /// Booleans are the boolean values returned from a query. + /// Booleans are boolean values. Boolean(value: Bool) - /// Texts are the text values returned from a query. + /// Texts are text values. Text(value: String) - /// Datetimes are the datetime values returned from a query (as Strings). + /// Datetimes are datetime values. Datetime(value: String) - /// Blobs are the blob values returned from a query (as Strings). + /// Blobs are blob values. Blob(value: String) - /// Nulls are the null values returned from a query. + /// Nulls are null values. Null } diff --git a/test/glibsql/http_test.gleam b/test/glibsql/http_test.gleam index ac9bbd0..5130586 100644 --- a/test/glibsql/http_test.gleam +++ b/test/glibsql/http_test.gleam @@ -1,5 +1,6 @@ import gleam/http import gleam/http/request as http_request +import gleam/option.{None, Some} import gleeunit/should import glibsql/http as glibsql @@ -13,7 +14,7 @@ pub fn builder_custom_host_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.6.0") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") |> http_request.set_body("{\"baton\":null,\"requests\":[]}") glibsql.new_request() @@ -36,7 +37,7 @@ pub fn builder_no_statements_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.6.0") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") |> http_request.set_body("{\"baton\":null,\"requests\":[]}") glibsql.new_request() @@ -57,16 +58,19 @@ pub fn builder_single_statement_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.6.0") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") |> http_request.set_body( - "{\"baton\":null,\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM users\"}},{\"type\":\"close\"}]}", + "{\"baton\":null,\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM users\",\"args\":[],\"named_args\":[]}},{\"type\":\"close\"}]}", ) glibsql.new_request() |> glibsql.with_database("database") |> glibsql.with_organization("organization") |> glibsql.with_token("token") - |> glibsql.with_statement(glibsql.ExecuteStatement(sql: "SELECT * FROM users")) + |> glibsql.with_statement(glibsql.ExecuteStatement( + query: "SELECT * FROM users", + arguments: None, + )) |> glibsql.with_statement(glibsql.CloseStatement) |> glibsql.build |> should.equal(Ok(expected)) @@ -82,17 +86,23 @@ pub fn builder_many_statement_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.6.0") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") |> http_request.set_body( - "{\"baton\":null,\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM users\"}},{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM posts\"}},{\"type\":\"close\"}]}", + "{\"baton\":null,\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM users\",\"args\":[],\"named_args\":[]}},{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM posts\",\"args\":[],\"named_args\":[]}},{\"type\":\"close\"}]}", ) glibsql.new_request() |> glibsql.with_database("database") |> glibsql.with_organization("organization") |> glibsql.with_token("token") - |> glibsql.with_statement(glibsql.ExecuteStatement(sql: "SELECT * FROM users")) - |> glibsql.with_statement(glibsql.ExecuteStatement(sql: "SELECT * FROM posts")) + |> glibsql.with_statement(glibsql.ExecuteStatement( + query: "SELECT * FROM users", + arguments: None, + )) + |> glibsql.with_statement(glibsql.ExecuteStatement( + query: "SELECT * FROM posts", + arguments: None, + )) |> glibsql.with_statement(glibsql.CloseStatement) |> glibsql.build |> should.equal(Ok(expected)) @@ -108,15 +118,21 @@ pub fn builder_clear_statements_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.6.0") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") |> http_request.set_body("{\"baton\":null,\"requests\":[]}") glibsql.new_request() |> glibsql.with_database("database") |> glibsql.with_organization("organization") |> glibsql.with_token("token") - |> glibsql.with_statement(glibsql.ExecuteStatement(sql: "SELECT * FROM users")) - |> glibsql.with_statement(glibsql.ExecuteStatement(sql: "SELECT * FROM posts")) + |> glibsql.with_statement(glibsql.ExecuteStatement( + query: "SELECT * FROM users", + arguments: None, + )) + |> glibsql.with_statement(glibsql.ExecuteStatement( + query: "SELECT * FROM posts", + arguments: None, + )) |> glibsql.with_statement(glibsql.CloseStatement) |> glibsql.clear_statements |> glibsql.build @@ -133,7 +149,7 @@ pub fn builder_baton_test() { |> http_request.set_header("Authorization", "Bearer token") |> http_request.set_header("Content-Type", "application/json") |> http_request.set_header("Accept", "application/json") - |> http_request.set_header("User-Agent", "glibsql/0.6.0") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") |> http_request.set_body( "{\"baton\":\"baton\",\"requests\":[{\"type\":\"close\"}]}", ) @@ -153,3 +169,63 @@ pub fn builder_missing_fields_test() { |> glibsql.build |> should.be_error } + +pub fn builder_anonymous_arguments_statement_test() { + let expected = + http_request.new() + |> http_request.set_method(http.Post) + |> http_request.set_scheme(http.Https) + |> http_request.set_host("database-organization.turso.io") + |> http_request.set_path("/v2/pipeline") + |> http_request.set_header("Authorization", "Bearer token") + |> http_request.set_header("Content-Type", "application/json") + |> http_request.set_header("Accept", "application/json") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") + |> http_request.set_body( + "{\"baton\":null,\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM users WHERE id = ?\",\"args\":[{\"type\":\"integer\",\"value\":\"1\"}],\"named_args\":[]}},{\"type\":\"close\"}]}", + ) + + glibsql.new_request() + |> glibsql.with_database("database") + |> glibsql.with_organization("organization") + |> glibsql.with_token("token") + |> glibsql.with_statement(glibsql.ExecuteStatement( + query: "SELECT * FROM users WHERE id = ?", + arguments: Some([ + glibsql.AnonymousArgument(value: glibsql.Integer(value: 1)), + ]), + )) + |> glibsql.with_statement(glibsql.CloseStatement) + |> glibsql.build + |> should.equal(Ok(expected)) +} + +pub fn builder_named_arguments_statement_test() { + let expected = + http_request.new() + |> http_request.set_method(http.Post) + |> http_request.set_scheme(http.Https) + |> http_request.set_host("database-organization.turso.io") + |> http_request.set_path("/v2/pipeline") + |> http_request.set_header("Authorization", "Bearer token") + |> http_request.set_header("Content-Type", "application/json") + |> http_request.set_header("Accept", "application/json") + |> http_request.set_header("User-Agent", "glibsql/0.7.0") + |> http_request.set_body( + "{\"baton\":null,\"requests\":[{\"type\":\"execute\",\"stmt\":{\"sql\":\"SELECT * FROM users WHERE id = :id\",\"args\":[],\"named_args\":[{\"name\":\"id\",\"value\":{\"type\":\"integer\",\"value\":\"1\"}}]}},{\"type\":\"close\"}]}", + ) + + glibsql.new_request() + |> glibsql.with_database("database") + |> glibsql.with_organization("organization") + |> glibsql.with_token("token") + |> glibsql.with_statement(glibsql.ExecuteStatement( + query: "SELECT * FROM users WHERE id = :id", + arguments: Some([ + glibsql.NamedArgument(name: "id", value: glibsql.Integer(value: 1)), + ]), + )) + |> glibsql.with_statement(glibsql.CloseStatement) + |> glibsql.build + |> should.equal(Ok(expected)) +}