diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d00b2d6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,88 @@ +repos: + # General checks + - repo: local + hooks: + - name: Prevent committing to main + id: no-commit-to-branch + language: system + entry: no-commit-to-branch + args: [--branch, main] + pass_filenames: false + - name: Make sure files end with a newline character + id: end-of-file-fixer + language: system + entry: end-of-file-fixer + types: [text] + - name: Remove trailing whitespace + id: trailing-whitespace-fixer + language: system + entry: trailing-whitespace-fixer + types: [text] + - name: Check for files that would conflict on case-insensitive filesystem + id: check-case-conflict + language: system + entry: check-case-conflict + - name: Check for merge conflicts + id: check-merge-conflict + language: system + entry: check-merge-conflict + - name: Check executable files have a shebang + id: check-executables-have-shebangs + language: system + entry: check-executables-have-shebangs + types: [executable] + - name: Check scripts with a shebang are executable + id: check-shebang-scripts-are-executable + language: system + entry: check-shebang-scripts-are-executable + - name: Don't allow adding large files + id: check-added-large-files + language: system + entry: check-added-large-files + + # Roc + - repo: https://github.com/hasnep/pre-commit-roc + rev: v0.1.0 + hooks: + - name: Lint Roc files + id: check + args: [src/main.roc] + - name: Format Roc files + id: format + + # YAML + - repo: local + hooks: + - name: Format YAML files + id: yaml-format + language: system + entry: prettier --write + types: [yaml] + + # Markdown + - repo: local + hooks: + - name: Format markdown files + id: markdown-format + language: system + entry: prettier --write + types: [markdown] + + # GitHub Actions + - repo: local + hooks: + - name: Validate GitHub Actions workflow files + id: github-workflows-check + language: system + entry: actionlint + types: [yaml] + files: \.github/workflows/.*\.ya?ml$ + + # Nix + - repo: local + hooks: + - name: Format Nix files + id: nix-format + language: system + entry: nixfmt + types: [nix] diff --git a/flake.lock b/flake.lock index a305964..327a179 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1732722421, - "narHash": "sha256-HRJ/18p+WoXpWJkcdsk9St5ZiukCqSDgbOGFa8Okehg=", + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", "owner": "edolstra", "repo": "flake-compat", - "rev": "9ed2ac151eada2306ca8c418ebd97807bb08f6ac", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "type": "github" }, "original": { @@ -16,27 +16,27 @@ "type": "github" } }, - "flake-utils": { + "flake-parts": { "inputs": { - "systems": "systems" + "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "lastModified": 1736143030, + "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "hercules-ci", + "repo": "flake-parts", "type": "github" } }, - "flake-utils_2": { + "flake-utils": { "inputs": { - "systems": "systems_2" + "systems": "systems" }, "locked": { "lastModified": 1731533236, @@ -78,6 +78,34 @@ } }, "nixpkgs": { + "locked": { + "lastModified": 1737469691, + "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1735774519, + "narHash": "sha256-CewEm1o2eVAnoqb6Ml+Qi9Gg/EfNAxbRx1lANGVyoLI=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/e9b51731911566bbf7e4895475a87fe06961de0b.tar.gz" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1722403750, "narHash": "sha256-tRmn6UiFAPX0m9G1AVcEPjWEOc9BtGsxGcs7Bz3MpsM=", @@ -96,17 +124,17 @@ "roc": { "inputs": { "flake-compat": "flake-compat", - "flake-utils": "flake-utils_2", + "flake-utils": "flake-utils", "nixgl": "nixgl", - "nixpkgs": "nixpkgs", + "nixpkgs": "nixpkgs_2", "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1735423545, - "narHash": "sha256-wO9P141TDsCpqifLJDJTuOAorXNkmqfpQ924dIaOjt8=", + "lastModified": 1737688899, + "narHash": "sha256-2Ec5PwUc7+rEqIQ7qet2c1zKWC74lF4tbeS8NpGQ3Qc=", "owner": "roc-lang", "repo": "roc", - "rev": "06e78daa91a2c192c774796a3f906ae9dd0f889e", + "rev": "cf5054c836814cd7596803f9874892efb4f47c52", "type": "github" }, "original": { @@ -117,11 +145,8 @@ }, "root": { "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": [ - "roc", - "nixpkgs" - ], + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", "roc": "roc" } }, @@ -133,11 +158,11 @@ ] }, "locked": { - "lastModified": 1732802692, - "narHash": "sha256-kFrxb45qj52TT/OFUFyTdmvXkn/KXDUL0/DOtjHEQvs=", + "lastModified": 1736303309, + "narHash": "sha256-IKrk7RL+Q/2NC6+Ql6dwwCNZI6T6JH2grTdJaVWHF0A=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "34971069ec33755b2adf2481851f66d8ec9a6bfa", + "rev": "a0b81d4fa349d9af1765b0f0b4a899c13776f706", "type": "github" }, "original": { @@ -160,21 +185,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 10d520c..73959e0 100644 --- a/flake.nix +++ b/flake.nix @@ -1,30 +1,50 @@ { - description = "devShell flake"; - inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; roc.url = "github:roc-lang/roc"; - nixpkgs.follows = "roc/nixpkgs"; - - # to easily make configs for multiple architectures - flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, roc, flake-utils }: - let supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; - in flake-utils.lib.eachSystem supportedSystems (system: - let - pkgs = import nixpkgs { inherit system; }; - rocPkgs = roc.packages.${system}; - - sharedInputs = (with pkgs; [ - rocPkgs.cli - ]); - in { + nixConfig = { + extra-trusted-public-keys = "roc-lang.cachix.org-1:6lZeqLP9SadjmUbskJAvcdGR2T5ViR57pDVkxJQb8R4="; + extra-trusted-substituters = "https://roc-lang.cachix.org"; + }; - devShell = pkgs.mkShell { - buildInputs = sharedInputs; + outputs = + inputs@{ + self, + nixpkgs, + flake-parts, + roc, + ... + }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-darwin" + "x86_64-linux" + ]; + perSystem = + { inputs', pkgs, ... }: + { + devShells.default = pkgs.mkShell { + name = "roc-html"; + packages = [ + inputs'.roc.packages.cli + pkgs.actionlint + pkgs.check-jsonschema + pkgs.fd + pkgs.just + pkgs.nixfmt-rfc-style + pkgs.nodePackages.prettier + pkgs.pre-commit + pkgs.python312Packages.pre-commit-hooks + pkgs.ratchet + ]; + shellHook = "pre-commit install --overwrite"; + }; + formatter = pkgs.nixfmt-rfc-style; }; - - formatter = pkgs.nixpkgs-fmt; - }); + }; } diff --git a/src/Controllers/Product.roc b/src/Controllers/Product.roc index e59518c..c677a7e 100644 --- a/src/Controllers/Product.roc +++ b/src/Controllers/Product.roc @@ -13,37 +13,45 @@ handle_routes! : db_path : Str, } => Result Response _ -handle_routes! = \{ req, url_segments, db_path } -> +handle_routes! = |{ req, url_segments, db_path }| query_params = req.uri |> Helpers.parse_query_params - |> Result.withDefault (Dict.empty {}) + |> Result.with_default(Dict.empty({})) partial = query_params - |> Dict.get "partial" - |> Result.map \val -> if val == "true" then Bool.true else Bool.false - |> Result.withDefault Bool.false + |> Dict.get("partial") + |> Result.map_ok(|val| if val == "true" then Bool.true else Bool.false) + |> Result.with_default(Bool.false) when (req.method, url_segments) is (GET, []) -> - products = Sql.Product.list!? { db_path } + products = Sql.Product.list!({ db_path })? - view = Views.Pages.pageProducts { - products, - } + view = Views.Pages.page_products( + { + products, + }, + ) if partial then view - |> Helpers.respond_template! 200 [ - { name: "HX-Push-Url", value: "/products" }, - ] + |> Helpers.respond_template!( + 200, + [ + { name: "HX-Push-Url", value: "/products" }, + ], + ) else view |> Views.Layout.sidebar - |> Helpers.respond_template! 200 [ - { name: "HX-Push-Url", value: "/products" }, - ] - - _ -> Err (NotHandled req) + |> Helpers.respond_template!( + 200, + [ + { name: "HX-Push-Url", value: "/products" }, + ], + ) + + _ -> Err(NotHandled(req)) diff --git a/src/Controllers/User.roc b/src/Controllers/User.roc index b5a6b29..f74d74d 100644 --- a/src/Controllers/User.roc +++ b/src/Controllers/User.roc @@ -14,37 +14,41 @@ handle_routes! : db_path : Str, } => Result Response _ -handle_routes! = \{ req, url_segments, db_path } -> +handle_routes! = |{ req, url_segments, db_path }| query_params = req.uri |> Helpers.parse_query_params - |> Result.withDefault (Dict.empty {}) + |> Result.with_default(Dict.empty({})) partial = query_params - |> Dict.get "partial" - |> Result.map \val -> if val == "true" then Bool.true else Bool.false - |> Result.withDefault Bool.false + |> Dict.get("partial") + |> Result.map_ok(|val| if val == "true" then Bool.true else Bool.false) + |> Result.with_default(Bool.false) when (req.method, url_segments) is (GET, []) -> - users = Sql.User.list!? { db_path } + users = Sql.User.list!({ db_path })? - view = Views.Pages.pageUsers { - users, - } + view = Views.Pages.page_users({ users }) if partial then view - |> Helpers.respond_template! 200 [ - { name: "HX-Push-Url", value: "/users" }, - ] + |> Helpers.respond_template!( + 200, + [ + { name: "HX-Push-Url", value: "/users" }, + ], + ) else view |> Views.Layout.sidebar - |> Helpers.respond_template! 200 [ - { name: "HX-Push-Url", value: "/users" }, - ] - - _ -> Err (NotHandled req) + |> Helpers.respond_template!( + 200, + [ + { name: "HX-Push-Url", value: "/users" }, + ], + ) + + _ -> Err(NotHandled(req)) diff --git a/src/Helpers.roc b/src/Helpers.roc index 1be30c3..649db22 100644 --- a/src/Helpers.roc +++ b/src/Helpers.roc @@ -17,114 +17,125 @@ import web.MultipartFormData exposing [parse_form_url_encoded] import html.Html respond_redirect! : Str => Result Response []_ -respond_redirect! = \next -> - Ok { - status: 303, - headers: [ - { name: "Location", value: next }, - ], - body: [], - } +respond_redirect! = |next| + Ok( + { + status: 303, + headers: [ + { name: "Location", value: next }, + ], + body: [], + }, + ) respond_html! : Html.Node, List { name : Str, value : Str } => Result Response []_ -respond_html! = \node, other_headers -> - Ok { - status: 200, - headers: [ - { name: "Content-Type", value: "text/html; charset=utf-8" }, - ] - |> List.concat other_headers, - body: Str.toUtf8 (Html.render node), - } +respond_html! = |node, other_headers| + Ok( + { + status: 200, + headers: [ + { name: "Content-Type", value: "text/html; charset=utf-8" }, + ] + |> List.concat(other_headers), + body: Str.to_utf8(Html.render(node)), + }, + ) decode_form_values : List U8 -> Result (Dict Str Str) _ -decode_form_values = \body -> - parse_form_url_encoded body - |> Result.mapErr \BadUtf8 -> BadRequest InvalidFormEncoding +decode_form_values = |body| + parse_form_url_encoded(body) + |> Result.map_err(|BadUtf8| BadRequest(InvalidFormEncoding)) parse_query_params : Str -> Result (Dict Str Str) _ -parse_query_params = \url -> - when Str.splitOn url "?" is - [_, query_part] -> query_part |> Str.toUtf8 |> parse_form_url_encoded - parts -> Err (InvalidQuery (Inspect.toStr parts)) +parse_query_params = |url| + when Str.split_on(url, "?") is + [_, query_part] -> query_part |> Str.to_utf8 |> parse_form_url_encoded + parts -> Err(InvalidQuery(Inspect.to_str(parts))) query_params_to_str : Dict Str Str -> Str -query_params_to_str = \params -> - Dict.toList params - |> List.map \(k, v) -> "$(k)=$(v)" - |> Str.joinWith "&" +query_params_to_str = |params| + Dict.to_list(params) + |> List.map(|(k, v)| "${k}=${v}") + |> Str.join_with("&") expect "localhost:8000?port=8000&name=Luke" |> parse_query_params - |> Result.map query_params_to_str + |> Result.map_ok(query_params_to_str) == - Ok "port=8000&name=Luke" + Ok("port=8000&name=Luke") parse_paged_params : Dict Str Str -> Result { page : I64, items : I64 } _ -parse_paged_params = \query_params -> +parse_paged_params = |query_params| - maybe_page = query_params |> Dict.get "page" |> Result.try Str.toI64 - maybe_count = query_params |> Dict.get "items" |> Result.try Str.toI64 + maybe_page = query_params |> Dict.get("page") |> Result.try(Str.to_i64) + maybe_count = query_params |> Dict.get("items") |> Result.try(Str.to_i64) when (maybe_page, maybe_count) is - (Ok page, Ok items) if page >= 1 && items > 0 -> Ok { page, items } - _ -> Err InvalidPagedParams + (Ok(page), Ok(items)) if page >= 1 and items > 0 -> Ok({ page, items }) + _ -> Err(InvalidPagedParams) expect "/bigTask?page=22&items=33" |> parse_query_params - |> Result.try parse_paged_params + |> Result.try(parse_paged_params) == - Ok { page: 22, items: 33 } + Ok({ page: 22, items: 33 }) expect "/bigTask?page=0&count=33" |> parse_query_params - |> Result.try parse_paged_params + |> Result.try(parse_paged_params) == - Err InvalidPagedParams + Err(InvalidPagedParams) expect "/bigTask" |> parse_query_params - |> Result.try parse_paged_params + |> Result.try(parse_paged_params) == - Err (InvalidQuery "[\"/bigTask\"]") + Err(InvalidQuery("[\"/bigTask\"]")) replace_query_params : { url : Str, params : Dict Str Str } -> Str -replace_query_params = \{ url, params } -> - when Str.splitFirst url "?" is - Ok { before } if Dict.isEmpty params -> "$(before)" - Err NotFound if Dict.isEmpty params -> "$(url)" - Ok { before } -> "$(before)?$(query_params_to_str params)" - Err NotFound -> "$(url)?$(query_params_to_str params)" +replace_query_params = |{ url, params }| + when Str.split_first(url, "?") is + Ok({ before }) if Dict.is_empty(params) -> "${before}" + Err(NotFound) if Dict.is_empty(params) -> "${url}" + Ok({ before }) -> "${before}?${query_params_to_str(params)}" + Err(NotFound) -> "${url}?${query_params_to_str(params)}" -expect replace_query_params { url: "/bigTask", params: Dict.empty {} } == "/bigTask" -expect replace_query_params { url: "/bigTask?items=33", params: Dict.empty {} } == "/bigTask" -expect replace_query_params { url: "/bigTask?items=33", params: Dict.fromList [("page", "22")] } == "/bigTask?page=22" +expect replace_query_params({ url: "/bigTask", params: Dict.empty({}) }) == "/bigTask" +expect replace_query_params({ url: "/bigTask?items=33", params: Dict.empty({}) }) == "/bigTask" +expect replace_query_params({ url: "/bigTask?items=33", params: Dict.from_list([("page", "22")]) }) == "/bigTask?page=22" respond_template! : Str, U16, _ => Result Response []_ -respond_template! = \html, status, headers -> - Ok { - status, - headers: List.concat headers [ - { name: "Content-Type", value: "text/html; charset=utf-8" }, - ], - body: html |> Str.toUtf8, - } +respond_template! = |html, status, headers| + Ok( + { + status, + headers: List.concat( + headers, + [ + { name: "Content-Type", value: "text/html; charset=utf-8" }, + ], + ), + body: html |> Str.to_utf8, + }, + ) decode_multi_part_form_boundary : List { name : Str, value : Str } -> Result (List U8) _ -decode_multi_part_form_boundary = \headers -> +decode_multi_part_form_boundary = |headers| headers - |> List.keepIf \{ name } -> name == "Content-Type" || name == "content-type" + |> List.keep_if(|{ name }| name == "Content-Type" or name == "content-type") |> List.first - |> Result.mapErr \_ -> ExpectedContentTypeHeader headers - |> Result.try \{ value } -> - when Str.splitLast value "=" is - Ok { after } -> Ok (Str.toUtf8 after) - Err err -> Err (InvalidContentTypeHeader err value) + |> Result.map_err(|_| ExpectedContentTypeHeader(headers)) + |> Result.try( + |{ value }| + when Str.split_last(value, "=") is + Ok({ after }) -> Ok(Str.to_utf8(after)) + Err(err) -> Err(InvalidContentTypeHeader(err, value)), + ) info! : Str => Result {} _ -info! = \msg -> - Stdout.line! "\u(001b)[34mINFO:\u(001b)[0m $(msg)" +info! = |msg| + Stdout.line!("\u(001b)[34mINFO:\u(001b)[0m ${msg}") diff --git a/src/Models/Session.roc b/src/Models/Session.roc index e396439..0a6c2b4 100644 --- a/src/Models/Session.roc +++ b/src/Models/Session.roc @@ -9,8 +9,8 @@ Session : { } is_authenticated : [Guest, LoggedIn Str] -> Result {} [Unauthorized] -is_authenticated = \user -> +is_authenticated = |user| if user == Guest then - Err Unauthorized + Err(Unauthorized) else - Ok {} + Ok({}) diff --git a/src/Sql/Product.roc b/src/Sql/Product.roc index e89d572..972f9b9 100644 --- a/src/Sql/Product.roc +++ b/src/Sql/Product.roc @@ -1,10 +1,10 @@ module [list!] import Models.Product exposing [Product] -import web.SQLite3 +import web.Sqlite list! : { db_path : Str } => Result (List Product) _ -list! = \{ db_path } -> +list! = |{ db_path }| query = """ @@ -20,32 +20,22 @@ list! = \{ db_path } -> """ rows = - SQLite3.execute! { - path: db_path, - query, - bindings: [], - } - |> Result.mapErr? SqlErrGettingProducts + Sqlite.query_many!( + { + path: db_path, + query, + bindings: [], + rows: { Sqlite.decode_record <- + id: Sqlite.i64("id"), + name: Sqlite.str("name"), + category: Sqlite.str("category"), + technology: Sqlite.str("technology"), + description: Sqlite.str("description"), + price: Sqlite.str("price"), + discount: Sqlite.str("discount"), + }, + }, + ) + ? SqlErrGettingProducts - parse_product_rows rows [] - -parse_product_rows : List (List SQLite3.Value), List Product -> Result (List Product) _ -parse_product_rows = \rows, acc -> - when rows is - [] -> Ok acc - [[Integer id, String name, String category, String technology, String description, String price, String discount], .. as rest] -> - parse_product_rows - rest - ( - List.append acc { - id, - name, - category, - technology, - description, - price, - discount, - } - ) - - row -> Err (UnexpectedValues "unexpected values, got row $(Inspect.toStr row)") + Ok(rows) diff --git a/src/Sql/Session.roc b/src/Sql/Session.roc index 2a5cb59..3e776fd 100644 --- a/src/Sql/Session.roc +++ b/src/Sql/Session.roc @@ -1,67 +1,54 @@ -module [ - new, - parse, - get, -] - -import web.Http exposing [Request] -import web.SQLite3 -import Models.Session exposing [Session] - -new : Str -> Task I64 _ -new = \path -> - - query = - "INSERT INTO sessions (session_id) VALUES (abs(random()));" - - _ = - SQLite3.execute { path, query, bindings: [] } - |> Task.map_err! \err -> SqlError err - - rows = - { path, query: "SELECT last_insert_rowid();", bindings: [] } - |> SQLite3.execute - |> Task.on_err! \err -> SqlError err |> Task.err - - when rows is - [] -> Task.err (UnexpectedValues "unexpected values in new Session, got NIL rows") - [[Integer id], ..] -> Task.ok id - _ -> Task.err (UnexpectedValues "unexpected values in new Session, got $(Inspect.toStr rows)") - -parse : Request -> Result I64 [NoSessionCookie, InvalidSessionCookie] -parse = \req -> - when req.headers |> List.keep_if \req_header -> req_header.name == "cookie" is - [req_header] -> - Str.splitOn req_header.value "=" - |> List.get 1 - |> Result.try Str.toI64 - |> Result.mapErr \_ -> InvalidSessionCookie - - _ -> Err NoSessionCookie - -get : I64, Str -> Task Session _ -get = \session_id, path -> - - not_found_str = "NOT_FOUND" - - query = - """ - SELECT - sessions.session_id, - COALESCE(users.name,'$(not_found_str)') AS 'username' - FROM sessions - LEFT OUTER JOIN users - ON sessions.user_id = users.id - WHERE sessions.session_id = :sessionId; - """ - - bindings = [{ name: ":sessionId", value: Integer session_id }] - - rows = SQLite3.execute { path, query, bindings } |> Task.map_err! SqlErrGettingSession - - when rows is - [] -> Task.err SessionNotFound - [[Integer id, String _username], ..] -> - Task.ok { id, user: LoggedIn "Demo User" } - - _ -> Task.err (UnexpectedValues "unexpected values in get Session, got $(Inspect.toStr rows)") +# TODO RESTORE SESSIONS +# module [ +# new, +# parse, +# get, +# ] +# import web.Http exposing [Request] +# import web.Sqlite +# import Models.Session exposing [Session] +# new : Str -> Task I64 _ +# new = |path| +# query = +# "INSERT INTO sessions (session_id) VALUES (abs(random()));" +# _ = +# Sqlite.execute({ path, query, bindings: [] }) +# |> Task.map_err!(|err| SqlError(err)) +# rows = +# { path, query: "SELECT last_insert_rowid();", bindings: [] } +# |> Sqlite.execute +# |> Task.on_err!(|err| SqlError(err) |> Task.err) +# when rows is +# [] -> Task.err(UnexpectedValues("unexpected values in new Session, got NIL rows")) +# [[Integer(id)], ..] -> Task.ok(id) +# _ -> Task.err(UnexpectedValues("unexpected values in new Session, got ${Inspect.to_str(rows)}")) +# parse : Request -> Result I64 [NoSessionCookie, InvalidSessionCookie] +# parse = |req| +# when req.headers |> List.keep_if(|req_header| req_header.name == "cookie") is +# [req_header] -> +# Str.split_on(req_header.value, "=") +# |> List.get(1) +# |> Result.try(Str.to_i64) +# |> Result.map_err(|_| InvalidSessionCookie) +# _ -> Err(NoSessionCookie) +# get : I64, Str -> Task Session _ +# get = |session_id, path| +# not_found_str = "NOT_FOUND" +# query = +# """ +# SELECT +# sessions.session_id, +# COALESCE(users.name,'${not_found_str}') AS 'username' +# FROM sessions +# LEFT OUTER JOIN users +# ON sessions.user_id = users.id +# WHERE sessions.session_id = :sessionId; +# """ +# bindings = [{ name: ":sessionId", value: Integer(session_id) }] +# rows = Sqlite.execute({ path, query, bindings }) |> Task.map_err!(SqlErrGettingSession) +# when rows is +# [] -> Task.err(SessionNotFound) +# [[Integer(id), String(_username)], ..] -> +# Task.ok({ id, user: LoggedIn("Demo User") }) +# _ -> Task.err(UnexpectedValues("unexpected values in get Session, got ${Inspect.to_str(rows)}")) +module [] diff --git a/src/Sql/User.roc b/src/Sql/User.roc index 7324edc..af3f23f 100644 --- a/src/Sql/User.roc +++ b/src/Sql/User.roc @@ -1,10 +1,10 @@ module [list!] import Models.User exposing [User] -import web.SQLite3 +import web.Sqlite list! : { db_path : Str } => Result (List User) _ -list! = \{ db_path } -> +list! = |{ db_path }| query = """ @@ -20,34 +20,24 @@ list! = \{ db_path } -> FROM [users]; """ - rows = - SQLite3.execute! { - path: db_path, - query, - bindings: [], - } - |> Result.mapErr? SqlErrGettingUsers + users = + Sqlite.query_many!( + { + path: db_path, + query, + bindings: [], + rows: { Sqlite.decode_record <- + id: Sqlite.i64("id"), + name: Sqlite.str("name"), + avatar: Sqlite.str("avatar"), + email: Sqlite.str("email"), + biography: Sqlite.str("biography"), + position: Sqlite.str("position"), + country: Sqlite.str("country"), + status: Sqlite.str("status"), + }, + }, + ) + ? SqlErrGettingUsers - parse_user_rows rows [] - -parse_user_rows : List (List SQLite3.Value), List User -> Result (List User) _ -parse_user_rows = \rows, acc -> - when rows is - [] -> Ok acc - [[Integer id, String name, String avatar, String email, String biography, String position, String country, String status], .. as rest] -> - parse_user_rows - rest - ( - List.append acc { - id, - name, - avatar, - email, - biography, - position, - country, - status, - } - ) - - row -> Err (UnexpectedValues "unexpected values, got row $(Inspect.toStr row)") + Ok(users) diff --git a/src/Views/Layout.roc b/src/Views/Layout.roc index edf4457..b74e566 100644 --- a/src/Views/Layout.roc +++ b/src/Views/Layout.roc @@ -6,12 +6,14 @@ module [ import Views.Pages header_template : Str -header_template = Views.Pages.header { - authors: "Themesberg", - description: "Get started with a free and open-source admin dashboard layout built with Tailwind CSS and Flowbite featuring charts, widgets, CRUD layouts, authentication pages, and more", - stylesheet: Views.Pages.stylesheet {}, - title: "Tailwind CSS Admin Dashboard - Flowbite", -} +header_template = Views.Pages.header( + { + authors: "Themesberg", + description: "Get started with a free and open-source admin dashboard layout built with Tailwind CSS and Flowbite featuring charts, widgets, CRUD layouts, authentication pages, and more", + stylesheet: Views.Pages.stylesheet({}), + title: "Tailwind CSS Admin Dashboard - Flowbite", + }, +) # TODO restore the footer # footerTemplate : Str @@ -20,30 +22,38 @@ header_template = Views.Pages.header { # } navbar_template : Str -navbar_template = Views.Pages.navBar { - relURL: "", -} +navbar_template = Views.Pages.nav_bar( + { + rel_url: "", + }, +) sidebar_template : Str -sidebar_template = Views.Pages.sidebar { - ariaLabel: "Sidebar", -} +sidebar_template = Views.Pages.sidebar( + { + aria_label: "Sidebar", + }, +) normal : Str -> Str -normal = \content -> - Views.Pages.layoutNormal { - header: header_template, - content: content, - footer: "", - navbar: "", - } +normal = |content| + Views.Pages.layout_normal( + { + header: header_template, + content: content, + footer: "", + navbar: "", + }, + ) sidebar : Str -> Str -sidebar = \content -> - Views.Pages.layoutSidebar { - header: header_template, - content, - footer: "", - navbar: navbar_template, - sidebar: sidebar_template, - } +sidebar = |content| + Views.Pages.layout_sidebar( + { + header: header_template, + content, + footer: "", + navbar: navbar_template, + sidebar: sidebar_template, + }, + ) diff --git a/src/Views/Pages.roc b/src/Views/Pages.roc index ef385fa..4a321da 100644 --- a/src/Views/Pages.roc +++ b/src/Views/Pages.roc @@ -1,26 +1,26 @@ ## Generated by RTL https://github.com/isaacvando/rtl module [ error500, + page_profile_lock, error404, - navBar, - layoutNormal, - pageProducts, - pageResetPassword, - pageUsers, - layoutSidebar, - pageSignIn, - pageSignUp, + page_settings, + page_forgot_password, + layout_sidebar, error401, stylesheet, - pageSettings, + layout_normal, + nav_bar, footer, + page_reset_password, + page_sign_up, + page_sign_in, + page_products, sidebar, header, - pageProfileLock, - pageForgotPassword, + page_users, ] -error500 = \_model -> +error500 = |_model| """
""" -error404 = \_model -> +page_profile_lock = |_model| + """ +
+
+ + FlowBite Logo + Flowbite + + +
+
+
+ Bonnie image +

+ Bonnie Green +

+
+

+ Better to be safe than sorry. +

+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + """ + +error404 = |_model| """
""" -navBar = \model -> +page_settings = |_model| """ - +

+ User settings +

+
+ +
+
+
+ Jese picture +
+

+ Profile picture +

+
+ JPG, GIF or PNG. Max size of 800K +
+
+ + +
-
-
- - - """ - -layoutNormal = \model -> - """ - - - - $(model.header) - - $(model.navbar) - -
- $(model.content) $(model.footer) -
-
- - - - """ - -pageProducts = \model -> - [ - """ -
-
-
+

+ Language & Time +

- -

All products

+ +
-
-
-
- -
- -
-
- -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - """, - List.map model.products \product -> - """ - - - - - - - - - - - - - - - """ - |> Str.joinWith "", - """ - - -
-
- - -
-
- Product Name - - Technology - - Description - - ID - - Price - - Discount - - Actions -
-
- - -
-
-
$(product.name |> escapeHtml)
-
$(product.category |> escapeHtml)
-
$(product.technology |> escapeHtml)$(product.description |> escapeHtml)#$(Num.toStr product.id |> escapeHtml)$(product.price |> escapeHtml)$(product.discount |> escapeHtml) - - -
+
+
+

+ Social accounts +

+ +
+
-
- -
-
- - - - - - - Showing 1-20 of 2290 -
- -
- - -
- - """, - ] - |> Str.joinWith "" - -pageResetPassword = \_model -> - """ -
-
- - FlowBite Logo - Flowbite - - -
-

- Reset your password -

-
-
- - -
-
- - -
-
- - -
-
-
- -
-
- -
-
- -
-
-
-
- - """ - -pageUsers = \model -> - [ - """ -
-
-
-
- -

All users

-
-
-
- - """, - ] - |> Str.joinWith "" - -layoutSidebar = \model -> - """ - - - - $(model.header) - - $(model.navbar) - -
- $(model.sidebar)
- $(model.content) $(model.footer) -
-
- - - - """ - -pageSignIn = \_model -> - """ -
-
- - FlowBite Logo - Flowbite - - -
-

- Sign in to platform -

-
-
- - -
-
- - -
-
-
- -
-
- -
- Lost Password? -
- -
- Not registered? - Create account -
-
-
-
-
- - """ - -pageSignUp = \_model -> - """ -
-
- - FlowBite Logo - Flowbite - - -
-

- Create a Free Account -

-
-
- - -
-
- - -
-
- - -
-
-
- -
-
- -
-
- -
- Already have an account? - Login here -
-
-
-
-
- - """ - -error401 = \_model -> - """ -
-
-
- lock image -
-
-

- Unauthorized Access -

-

- Sorry, you do not have permission to access this page. If you - believe this is a mistake, please contact your administrator. -

- - - - - Go back home - -
-
-
- - """ - -stylesheet = \_model -> - """ - - - - - - """ - -pageSettings = \_model -> - """ -
-
-
- -

- User settings -

-
- -
-
-
- Jese picture -
-

- Profile picture -

-
- JPG, GIF or PNG. Max size of 800K -
-
- + +
+
-
-
-
-

- Language & Time -

-
- - -
-
- - -
-
- -
+

- Social accounts + Sessions

-
-
-

- Other accounts -

-
    -
  • +
+
+
+
+
+

+ Alerts & Notifications +

+

+ You can set up Themesberg to get notifications +

+
+ +
+
-
-
- Bonnie image -
-
-

- Bonnie Green -

-

- New York, USA -

-

- Last seen: 1 min ago -

-
-
- + Company News
- -
  • -
    -
    - Jese image -
    -
    -

    - Jese Leos -

    -

    - California, USA -

    -

    - Last seen: 2 min ago -

    -
    -
    - + Get Themesberg news, announcements, and product + updates
    -
  • -
  • +
  • + +
    + +
    +
    -
    -
    - Thomas image -
    -
    -

    - Thomas Lean -

    -

    - Texas, USA -

    -

    - Last seen: 1 hour ago -

    -
    -
    - + Account Activity
    - -
  • -
    -
    - Lana image -
    -
    -

    - Lana Byrd -

    -

    - Texas, USA -

    -

    - Last seen: 1 hour ago -

    -
    -
    - + Get important notifications about you or + activity you've missed
    -
  • - -
    - -
    -
    -
    -
    -
    -
    -

    - General information -

    -
    -
    -
    - -
    -
    - + +
    + +
    +
    +
    - -
    -
    - +
    - + Get an email when a Dribbble Meetup is posted + close to my location +
    -
    - +
    -
    - + +
    + +
    +
    +
    - -
    -
    - +
    - + Get Themsberg news, announcements, and product + updates +
    -
    - +
    -
    - - -
    -
    - + +
    +
    +
    + +
    +
    +
    +
    +
    +

    + Email Notifications +

    +

    + You can set up Themesberg to get email notifications +

    +
    + +
    +
    +
    - -
    -
    - +
    - + Send an email reminding me to rate an item a + week after purchase +
    -
    - +
    -
    - + +
    + +
    +
    +
    - -
    -
    - + Send user and product notifications for you +
    -
    - -
    -
    -

    - Password information -

    -
    -
    -
    - +
    -
    - + +
    + +
    +
    +
    + Item comment notifications +
    +
    + Send me an email when someone comments on one of + my items +
    +
    + +
    + +
    +
    -
    -
    - + Send me an email when someone leaves a review + with their rating +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + + """ + +page_forgot_password = |_model| + """ +
    +
    + + FlowBite Logo + Flowbite + + +
    +
    +

    + Forgot your password? +

    +

    + Don't fret! Just type in your email and we will send you a + code to reset your password! +

    + +
    + + +
    +
    +
    +
    -
    -
    +
    +
    +
    +
    + + """ + +layout_sidebar = |model| + """ + + + + ${model.header} + + ${model.navbar} + +
    + ${model.sidebar}
    -
    -

    - Sessions -

    -
      -
    • -
      -
      - - - -
      -
      -

      - California 123.123.123.123 -

      -

      - Chrome on macOS -

      -
      -
      - Revoke -
      -
      -
    • -
    • -
      -
      - - - -
      -
      -

      - Rome 24.456.355.98 -

      -

      - Safari on iPhone -

      -
      -
      - Revoke -
      -
      -
    • -
    -
    -
    + + + + """ + +error401 = |_model| + """ +
    +
    +
    + lock image +
    +
    +

    + Unauthorized Access +

    +

    + Sorry, you do not have permission to access this page. If you + believe this is a mistake, please contact your administrator. +

    + + + + + Go back home + +
    +
    +
    + + """ + +stylesheet = |_model| + """ + + + + + + """ + +layout_normal = |model| + """ + + + + ${model.header} + + ${model.navbar} + +
    +
    + ${model.content} ${model.footer} +
    +
    + + + + """ + +nav_bar = |model| + """ + + + """ + +footer = |model| + """ + +

    + © ${model.copyright |> escape_html} + Flowbite.com. All + rights reserved. +

    + + """ + +page_reset_password = |_model| + """ +
    +
    + + FlowBite Logo + Flowbite + + +
    +

    + Reset your password +

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + """ + +page_sign_up = |_model| + """ +
    +
    + + FlowBite Logo + Flowbite + + +
    +

    + Create a Free Account +

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    + - See more -
    -
    + +
    + Already have an account? + Login here +
    +
    -
    +
    + + """ + +page_sign_in = |_model| + """ +
    +
    + + FlowBite Logo + Flowbite + +
    -
    -

    - Alerts & Notifications -

    -

    - You can set up Themesberg to get notifications -

    -
    - -
    -
    -
    - Company News -
    -
    - Get Themesberg news, announcements, and product - updates -
    -
    - -
    - -
    -
    -
    - Account Activity -
    -
    - Get important notifications about you or - activity you've missed -
    -
    - -
    - -
    -
    -
    - Meetups Near You -
    -
    - Get an email when a Dribbble Meetup is posted - close to my location -
    -
    - +

    + Sign in to platform +

    +
    +
    + + +
    +
    + + +
    +
    +
    +
    - -
    -
    -
    - New Messages -
    -
    - Get Themsberg news, announcements, and product - updates -
    -
    +
    - - -
    + Lost Password?
    -
    - +
    + Not registered? + Create account - Save all -
    -
    +
    -
    -
    -

    - Email Notifications -

    -

    - You can set up Themesberg to get email notifications -

    -
    - -
    -
    -
    - Rating reminders +
    +
    + + """ + +page_products = |model| + [ + """ +
    +
    +
    +
    +
    - -
    - -
    -
    -
    - Item update notifications + + + +

    All products

    +
    +
    +
    +
    + +
    +
    -
    - Send user and product notifications for you + + -
    - -
    -
    -
    - Item comment notifications -
    -
    - Send me an email when someone comments on one of - my items -
    -
    - + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + """, + List.map model.products |product| + """ + + + + + + + + + + + + + + + """ + |> Str.join_with "", + """ + + +
    +
    + + +
    +
    + Product Name + + Technology + + Description + + ID + + Price + + Discount + + Actions +
    +
    + + +
    +
    +
    ${product.name |> escape_html}
    +
    ${product.category |> escape_html}
    +
    ${product.technology |> escape_html}${product.description |> escape_html}#${Num.to_str product.id |> escape_html}${product.price |> escape_html}${product.discount |> escape_html} + + +
    +
    +
    +
    +
    + +
    +
    + + + + + + + Showing 1-20 of 2290 +
    + +
    + + + -
    -
    - """ -footer = \model -> - """ - -

    - © $(model.copyright |> escapeHtml) - Flowbite.com. All - rights reserved. -

    + + + + + + + - """ + """, + ] + |> Str.join_with "" -sidebar = \model -> +sidebar = |model| """