a self-hosted recipe app to learn sql and gleam
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

total rework. full stack stuff. main logic now in server

stunwin.com 18cf0b03

+1222
+23
.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "28" 18 + gleam-version: "1.13.0" 19 + rebar3-version: "3" 20 + # elixir-version: "1" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check src test
+6
.gitignore
··· 1 + *.beam 2 + *.ez 3 + server/build 4 + client/build 5 + shared/build 6 + erl_crash.dump
+24
client/README.md
··· 1 + # client 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/client)](https://hex.pm/packages/client) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/client/) 5 + 6 + ```sh 7 + gleam add client@1 8 + ``` 9 + ```gleam 10 + import client 11 + 12 + pub fn main() -> Nil { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/client>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+27
client/gleam.toml
··· 1 + name = "client" 2 + version = "1.0.0" 3 + target = "javascript" 4 + 5 + # Fill out these fields if you intend to generate HTML documentation or publish 6 + # your project to the Hex package manager. 7 + # 8 + # description = "" 9 + # licences = ["Apache-2.0"] 10 + # repository = { type = "github", user = "", repo = "" } 11 + # links = [{ title = "Website", href = "" }] 12 + # 13 + # For a full reference of all the available options, you can have a look at 14 + # https://gleam.run/writing-gleam/gleam-toml/. 15 + 16 + [dependencies] 17 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 18 + lustre = ">= 5.4.0 and < 6.0.0" 19 + rsvp = ">= 1.1.3 and < 2.0.0" 20 + gleam_json = ">= 3.1.0 and < 4.0.0" 21 + gleam_http = ">= 4.3.0 and < 5.0.0" 22 + plinth = ">= 0.8.1 and < 1.0.0" 23 + shared = { path = "../shared" } 24 + 25 + [dev-dependencies] 26 + gleeunit = ">= 1.0.0 and < 2.0.0" 27 + lustre_dev_tools = ">= 2.3.2 and < 3.0.0"
+59
client/manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 + { name = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, 7 + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, 8 + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 9 + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 10 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 11 + { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, 12 + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, 13 + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 14 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 15 + { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" }, 16 + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 17 + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 18 + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, 19 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 20 + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 21 + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 22 + { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 23 + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, 24 + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 25 + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 26 + { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, 27 + { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, 28 + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 29 + { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, 30 + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 31 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 32 + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 33 + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 34 + { name = "lustre", version = "5.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "40E097BABCE65FB7C460C073078611F7F5802EB07E1A9BFB5C229F71B60F8E50" }, 35 + { name = "lustre_dev_tools", version = "2.3.2", build_tools = ["gleam"], requirements = ["argv", "booklet", "filepath", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_regexp", "gleam_stdlib", "glint", "group_registry", "justin", "lustre", "mist", "polly", "simplifile", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "A2D7751DC679199A0A6046016A1D9BE4516D5EC0101E2282E4D5005587A5CB9B" }, 36 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 37 + { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" }, 38 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 39 + { name = "plinth", version = "0.8.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "012E65087018F9C7B48513EB47D0F452D6E57AF257555B46BB82BC0B58D9C482" }, 40 + { name = "polly", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "simplifile"], otp_app = "polly", source = "hex", outer_checksum = "1BA4D0ACE9BCF52AEA6AD9DE020FD8220CCA399A379E50A1775FC5C1204FCF56" }, 41 + { name = "rsvp", version = "1.1.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "4A582C9C49B4EC3197631E78FDB4D0A8703F14043EC12EAAC608E7B9347C2211" }, 42 + { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], source = "local", path = "../shared" }, 43 + { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, 44 + { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, 45 + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 46 + { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, 47 + { name = "wisp", version = "2.1.1", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "46E2E31DECD61A3748CF6CB317D9AC432BBC8D8A6E65655A9E787BDC69389DE0" }, 48 + ] 49 + 50 + [requirements] 51 + gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 52 + gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 53 + gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 54 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 55 + lustre = { version = ">= 5.4.0 and < 6.0.0" } 56 + lustre_dev_tools = { version = ">= 2.3.2 and < 3.0.0" } 57 + plinth = { version = ">= 0.8.1 and < 1.0.0" } 58 + rsvp = { version = ">= 1.1.3 and < 2.0.0" } 59 + shared = { path = "../shared" }
+5
client/src/client.gleam
··· 1 + import gleam/io 2 + 3 + pub fn main() -> Nil { 4 + io.println("Hello from client!") 5 + }
+13
client/test/client_test.gleam
··· 1 + import gleeunit 2 + 3 + pub fn main() -> Nil { 4 + gleeunit.main() 5 + } 6 + 7 + // gleeunit test functions end in `_test` 8 + pub fn hello_world_test() { 9 + let name = "Joe" 10 + let greeting = "Hello, " <> name <> "!" 11 + 12 + assert greeting == "Hello, Joe!" 13 + }
+24
server/README.md
··· 1 + # server 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/server)](https://hex.pm/packages/server) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/server/) 5 + 6 + ```sh 7 + gleam add server@1 8 + ``` 9 + ```gleam 10 + import server 11 + 12 + pub fn main() -> Nil { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/server>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+7
server/birdie_snapshots/full_reciple_read.accepted
··· 1 + --- 2 + version: 1.5.3 3 + title: full reciple read 4 + file: ./test/server_test.gleam 5 + test_name: read_full_recipe_test 6 + --- 7 + Ok(RecipeDetailView("Soup", "Mom", 4, "It's soup, whaddya want?", [Ingredient("tomato", "cup", 2), Ingredient("onion", "cup", 1), Ingredient("olive oil", "tbsp", 1)], [Instruction(1, "Chop the onion."), Instruction(2, "Simmer everything until it tastes like soup."), Instruction(3, "Serve hot.")]))
+7
server/birdie_snapshots/indexed_recipes.accepted
··· 1 + --- 2 + version: 1.5.3 3 + title: indexed recipes 4 + file: ./test/server_test.gleam 5 + test_name: get_indexed_recipes_test 6 + --- 7 + Ok([RecipeStub(2, "pie"), RecipeStub(1, "Soup")])
+7
server/birdie_snapshots/read_recipe_ingredients_table.accepted
··· 1 + --- 2 + version: 1.5.3 3 + title: read recipe_ingredients table 4 + file: ./test/server_test.gleam 5 + test_name: recipe_ingredients_read_test 6 + --- 7 + Ok([#(1, 1, 1, 1, 2), #(2, 1, 2, 1, 1), #(3, 1, 3, 2, 1)])
+7
server/birdie_snapshots/read_recipe_table.accepted
··· 1 + --- 2 + version: 1.5.3 3 + title: read recipe table 4 + file: ./test/server_test.gleam 5 + test_name: recipe_read_test 6 + --- 7 + Ok([#(1, "Soup", 1, 4, "It's soup, whaddya want?")])
server/data/db.db

This is a binary file and will not be displayed.

+46
server/data/seed.sql
··· 1 + CREATE TABLE authors ( 2 + id INTEGER PRIMARY KEY, 3 + name TEXT UNIQUE COLLATE NOCASE NOT NULL 4 + 5 + ); 6 + 7 + CREATE TABLE ingredients ( 8 + id INTEGER PRIMARY KEY, 9 + name TEXT UNIQUE COLLATE NOCASE NOT NULL 10 + 11 + ); 12 + 13 + CREATE TABLE units ( 14 + id INTEGER PRIMARY KEY, 15 + name TEXT UNIQUE COLLATE NOCASE NOT NULL 16 + ); 17 + 18 + CREATE TABLE recipes ( 19 + id INTEGER PRIMARY KEY, 20 + title TEXT UNIQUE COLLATE NOCASE NOT NULL, 21 + author_id INTEGER REFERENCES authors(id) ON DELETE SET NULL, 22 + servings INT NOT NULL DEFAULT 4, 23 + description TEXT NOT NULL DEFAULT 'no description', 24 + UNIQUE(author_id, title) 25 + ); 26 + 27 + CREATE TABLE recipe_ingredients ( 28 + 29 + id INTEGER PRIMARY KEY, 30 + recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, 31 + ingredient_id INTEGER NOT NULL REFERENCES ingredients(id), 32 + unit_id INTEGER NOT NULL REFERENCES units(id), 33 + quantity INTEGER NOT NULL, 34 + UNIQUE(recipe_id, ingredient_id) 35 + 36 + ); 37 + CREATE TABLE recipe_instructions ( 38 + 39 + id INTEGER PRIMARY KEY, 40 + recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, 41 + step INTEGER NOT NULL, 42 + instruction TEXT NOT NULL, 43 + UNIQUE(recipe_id, step) 44 + 45 + ); 46 +
+106
server/data/test_db.sql
··· 1 + CREATE TABLE authors ( 2 + id INTEGER PRIMARY KEY, 3 + name TEXT UNIQUE COLLATE NOCASE NOT NULL 4 + 5 + ); 6 + 7 + CREATE TABLE ingredients ( 8 + id INTEGER PRIMARY KEY, 9 + name TEXT UNIQUE COLLATE NOCASE NOT NULL 10 + 11 + ); 12 + 13 + CREATE TABLE units ( 14 + id INTEGER PRIMARY KEY, 15 + name TEXT UNIQUE COLLATE NOCASE NOT NULL 16 + ); 17 + 18 + CREATE TABLE recipes ( 19 + id INTEGER PRIMARY KEY, 20 + title TEXT UNIQUE COLLATE NOCASE NOT NULL, 21 + author_id INTEGER REFERENCES authors(id) ON DELETE SET NULL, 22 + servings INT NOT NULL DEFAULT 4, 23 + description TEXT NOT NULL DEFAULT 'no description', 24 + UNIQUE(author_id, title) 25 + ); 26 + 27 + CREATE TABLE recipe_ingredients ( 28 + 29 + id INTEGER PRIMARY KEY, 30 + recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, 31 + ingredient_id INTEGER NOT NULL REFERENCES ingredients(id), 32 + unit_id INTEGER NOT NULL REFERENCES units(id), 33 + quantity INTEGER NOT NULL, 34 + UNIQUE(recipe_id, ingredient_id) 35 + 36 + ); 37 + CREATE TABLE recipe_instructions ( 38 + 39 + id INTEGER PRIMARY KEY, 40 + recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, 41 + step INTEGER NOT NULL, 42 + instruction TEXT NOT NULL, 43 + UNIQUE(recipe_id, step) 44 + 45 + ); 46 + 47 + pragma foreign_keys = on 48 + ; 49 + begin 50 + ; 51 + 52 + INSERT OR IGNORE INTO authors (name) VALUES ('Mom'); 53 + INSERT OR IGNORE INTO units (name) VALUES ('cup'); 54 + INSERT OR IGNORE INTO units (name) VALUES ('tbsp'); 55 + 56 + INSERT OR IGNORE INTO ingredients (name) VALUES ('tomato'); 57 + INSERT OR IGNORE INTO ingredients (name) VALUES ('onion'); 58 + INSERT OR IGNORE INTO ingredients (name) VALUES ('olive oil'); 59 + 60 + INSERT INTO recipes (title, author_id, servings, description) 61 + VALUES ( 62 + 'Soup', 63 + (SELECT id FROM authors WHERE name = 'Mom'), 64 + 4, 65 + 'It''s soup, whaddya want?' 66 + ) 67 + ON CONFLICT(title) DO UPDATE SET 68 + author_id = excluded.author_id, 69 + servings = excluded.servings, 70 + description = excluded.description; 71 + 72 + INSERT INTO recipe_ingredients (recipe_id, ingredient_id, unit_id, quantity) 73 + VALUES 74 + ( 75 + (SELECT id FROM recipes WHERE title = 'Soup'), 76 + (SELECT id FROM ingredients WHERE name = 'tomato'), 77 + (SELECT id FROM units WHERE name = 'cup'), 78 + 2 79 + ), 80 + ( 81 + (SELECT id FROM recipes WHERE title = 'Soup'), 82 + (SELECT id FROM ingredients WHERE name = 'onion'), 83 + (SELECT id FROM units WHERE name = 'cup'), 84 + 1 85 + ), 86 + ( 87 + (SELECT id FROM recipes WHERE title = 'Soup'), 88 + (SELECT id FROM ingredients WHERE name = 'olive oil'), 89 + (SELECT id FROM units WHERE name = 'tbsp'), 90 + 1 91 + ) 92 + ON CONFLICT(recipe_id, ingredient_id) DO UPDATE SET 93 + unit_id = excluded.unit_id, 94 + quantity = excluded.quantity; 95 + 96 + INSERT INTO recipe_instructions (recipe_id, step, instruction) 97 + VALUES 98 + ((SELECT id FROM recipes WHERE title = 'Soup'), 1, 'Chop the onion.'), 99 + ((SELECT id FROM recipes WHERE title = 'Soup'), 2, 'Simmer everything until it tastes like soup.'), 100 + ((SELECT id FROM recipes WHERE title = 'Soup'), 3, 'Serve hot.') 101 + ON CONFLICT(recipe_id, step) DO UPDATE SET 102 + instruction = excluded.instruction; 103 + 104 + commit 105 + ; 106 +
+29
server/gleam.toml
··· 1 + name = "server" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "", repo = "" } 10 + # links = [{ title = "Website", href = "" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 + sqlight = ">= 1.0.3 and < 2.0.0" 18 + simplifile = ">= 2.3.1 and < 3.0.0" 19 + wisp = ">= 2.1.1 and < 3.0.0" 20 + shared = { path = "../shared" } 21 + gleam_erlang = ">= 1.3.0 and < 2.0.0" 22 + gleam_http = ">= 4.3.0 and < 5.0.0" 23 + gleam_json = ">= 3.1.0 and < 4.0.0" 24 + mist = ">= 5.0.3 and < 6.0.0" 25 + lustre = ">= 5.4.0 and < 6.0.0" 26 + 27 + [dev-dependencies] 28 + gleeunit = ">= 1.0.0 and < 2.0.0" 29 + birdie = ">= 1.5.3 and < 2.0.0"
+62
server/manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 + { name = "birdie", version = "1.5.3", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "envoy", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "global_value", "justin", "rank", "simplifile", "term_size", "tom", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "64E7D9CC9E84272DA07061628E1B8F31F34FCD2008BCED47AB8FD58457CA63E2" }, 7 + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, 8 + { name = "edit_distance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "7DC465C34695F9E57D79FC65670C53C992CE342BF29E0AA41FF44F61AF62FC56" }, 9 + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 10 + { name = "esqlite", version = "0.9.0", build_tools = ["rebar3"], requirements = [], otp_app = "esqlite", source = "hex", outer_checksum = "CCF72258A4EE152EC7AD92AA9A03552EB6CA1B06B65C93AD5B6E55C302E05855" }, 11 + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 12 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 13 + { name = "glance", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "49E0ED4793BB3F56C3E5ED00528D70CAE21D263F70A735604124B95C5F62E2DB" }, 14 + { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, 15 + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, 16 + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 17 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 18 + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 19 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 20 + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 21 + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 22 + { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 23 + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, 24 + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 25 + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 26 + { name = "glexer", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "40A1FB0919FA080AD6C5809B4C7DBA545841CAAC8168FACDFA0B0667C22475CC" }, 27 + { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, 28 + { name = "global_value", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "global_value", source = "hex", outer_checksum = "23F74C91A7B819C43ABCCBF49DAD5BB8799D81F2A3736BA9A534BD47F309FF4F" }, 29 + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 30 + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 31 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 32 + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 33 + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 34 + { name = "lustre", version = "5.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "40E097BABCE65FB7C460C073078611F7F5802EB07E1A9BFB5C229F71B60F8E50" }, 35 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 36 + { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" }, 37 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 38 + { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, 39 + { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], source = "local", path = "../shared" }, 40 + { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, 41 + { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, 42 + { name = "sqlight", version = "1.0.3", build_tools = ["gleam"], requirements = ["esqlite", "gleam_stdlib"], otp_app = "sqlight", source = "hex", outer_checksum = "CADD79663C9B61D4BAC960A47CC2D42CA8F48EAF5804DBEB79977287750F4B16" }, 43 + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 44 + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, 45 + { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, 46 + { name = "trie_again", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "E3BD66B4E126EF567EA8C4944EAB216413392ADF6C16C36047AF79EE5EF13466" }, 47 + { name = "wisp", version = "2.1.1", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "46E2E31DECD61A3748CF6CB317D9AC432BBC8D8A6E65655A9E787BDC69389DE0" }, 48 + ] 49 + 50 + [requirements] 51 + birdie = { version = ">= 1.5.3 and < 2.0.0" } 52 + gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } 53 + gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 54 + gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 55 + gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 56 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 57 + lustre = { version = ">= 5.4.0 and < 6.0.0" } 58 + mist = { version = ">= 5.0.3 and < 6.0.0" } 59 + shared = { path = "../shared" } 60 + simplifile = { version = ">= 2.3.1 and < 3.0.0" } 61 + sqlight = { version = ">= 1.0.3 and < 2.0.0" } 62 + wisp = { version = ">= 2.1.1 and < 3.0.0" }
+140
server/src/queries/read.gleam
··· 1 + import gleam/dynamic/decode 2 + import gleam/result 3 + import shared/types as t 4 + import sqlight 5 + 6 + pub fn decode_int() { 7 + use id <- decode.field(0, decode.int) 8 + decode.success(id) 9 + } 10 + 11 + pub fn single_col_decode() { 12 + use id <- decode.field(0, decode.int) 13 + use name <- decode.field(1, decode.string) 14 + decode.success(#(id, name)) 15 + } 16 + 17 + pub fn recipe_decode() { 18 + use id <- decode.field(0, decode.int) 19 + use title <- decode.field(1, decode.string) 20 + use author_id <- decode.field(2, decode.int) 21 + use servings <- decode.field(3, decode.int) 22 + use description <- decode.field(4, decode.string) 23 + decode.success(#(id, title, author_id, servings, description)) 24 + } 25 + 26 + pub fn recipe_ingredient_decode() { 27 + use id <- decode.field(0, decode.int) 28 + use recipe_id <- decode.field(1, decode.int) 29 + use ingredient_id <- decode.field(2, decode.int) 30 + use unit_id <- decode.field(3, decode.int) 31 + use quantity <- decode.field(4, decode.int) 32 + decode.success(#(id, recipe_id, ingredient_id, unit_id, quantity)) 33 + } 34 + 35 + pub fn recipe_view_decode() { 36 + use id <- decode.field(0, decode.int) 37 + use title <- decode.field(1, decode.string) 38 + use author <- decode.field(2, decode.string) 39 + use servings <- decode.field(3, decode.int) 40 + use description <- decode.field(4, decode.string) 41 + decode.success(t.RecipeRow(id:, title:, author:, servings:, description:)) 42 + } 43 + 44 + pub fn ingredient_view_decode() { 45 + use name <- decode.field(0, decode.string) 46 + use unit <- decode.field(1, decode.string) 47 + use quantity <- decode.field(2, decode.int) 48 + decode.success(t.Ingredient(name, unit, quantity)) 49 + } 50 + 51 + pub fn instruction_decode() { 52 + use id <- decode.field(0, decode.int) 53 + use recipe_id <- decode.field(1, decode.int) 54 + use step <- decode.field(3, decode.int) 55 + use instruction <- decode.field(4, decode.string) 56 + decode.success(#(id, recipe_id, step, instruction)) 57 + } 58 + 59 + pub fn instruction_view_decode() { 60 + use step <- decode.field(0, decode.int) 61 + use instruction <- decode.field(1, decode.string) 62 + decode.success(t.Instruction(step:, instruction:)) 63 + } 64 + 65 + pub fn recipe_stub_decode() { 66 + use recipe_id <- decode.field(0, decode.int) 67 + use title <- decode.field(1, decode.string) 68 + decode.success(t.RecipeStub(recipe_id:, title:)) 69 + } 70 + 71 + pub fn get_indexed_recipes( 72 + conn: sqlight.Connection, 73 + ) -> Result(List(t.RecipeStub), sqlight.Error) { 74 + let sql = "SELECT id, title FROM recipes" 75 + sqlight.query(sql, on: conn, with: [], expecting: recipe_stub_decode()) 76 + } 77 + 78 + pub fn get_full_recipe( 79 + name: String, 80 + conn: sqlight.Connection, 81 + ) -> Result(t.RecipeDetailView, sqlight.Error) { 82 + // start with the name, pull the recipe record 83 + let sql = 84 + " 85 + SELECT 86 + r.id, 87 + r.title, 88 + COALESCE(a.name, '') AS author, 89 + r.servings, 90 + r.description 91 + FROM recipes r 92 + LEFT JOIN authors a ON a.id = r.author_id 93 + WHERE r.title = ?; 94 + " 95 + use raw_recipe <- result.try(sqlight.query( 96 + sql, 97 + conn, 98 + [sqlight.text(name)], 99 + recipe_view_decode(), 100 + )) 101 + let assert [recipe] = raw_recipe 102 + 103 + let sql = 104 + " 105 + SELECT 106 + i.name AS ingredient_name, 107 + u.name AS unit_name, 108 + ri.quantity AS quantity 109 + FROM recipe_ingredients ri 110 + JOIN ingredients i ON i.id = ri.ingredient_id 111 + JOIN units u ON u.id = ri.unit_id 112 + WHERE ri.recipe_id = ? 113 + ORDER BY ri.id; 114 + " 115 + use ingredient_list <- result.try(sqlight.query( 116 + sql, 117 + conn, 118 + [sqlight.int(recipe.id)], 119 + ingredient_view_decode(), 120 + )) 121 + 122 + let sql = 123 + "SELECT step, instruction FROM recipe_instructions WHERE recipe_id = ?" 124 + 125 + use instruction_list <- result.map(sqlight.query( 126 + sql, 127 + conn, 128 + [sqlight.int(recipe.id)], 129 + instruction_view_decode(), 130 + )) 131 + 132 + t.RecipeDetailView( 133 + title: recipe.title, 134 + author: recipe.author, 135 + servings: recipe.servings, 136 + description: recipe.description, 137 + ingredients: ingredient_list, 138 + instructions: instruction_list, 139 + ) 140 + }
+171
server/src/queries/write.gleam
··· 1 + import gleam/list 2 + import gleam/string 3 + import queries/read 4 + import simplifile 5 + import sqlight 6 + 7 + import shared/types as t 8 + 9 + /// Takes a list of Ingredient type, along with the rowid of a recipe(normally passed in through the write_full_recipe function. If you're calling it separately, have the recipe_id handy from a previous query.) 10 + /// 11 + pub fn write_recipe_ingredients( 12 + ingredients: List(t.Ingredient), 13 + recipe_id: Int, 14 + conn: sqlight.Connection, 15 + ) { 16 + let ingredient_names = ingredients |> list.map(fn(i) { i.name }) 17 + write_single_column(ingredient_names, t.Ingredients, conn) 18 + 19 + let unit_names = ingredients |> list.map(fn(i) { i.unit }) 20 + write_single_column(unit_names, t.Units, conn) 21 + 22 + let sql = 23 + "INSERT INTO recipe_ingredients (recipe_id, ingredient_id, unit_id, quantity) VALUES" 24 + <> list.repeat( 25 + "(?, 26 + (SELECT id FROM ingredients WHERE name = ?), 27 + (SELECT id FROM units WHERE name = ?), 28 + ?)", 29 + list.length(ingredients), 30 + ) 31 + |> string.join(",") 32 + let args = 33 + list.flat_map(ingredients, fn(ingredient) { 34 + [ 35 + sqlight.int(recipe_id), 36 + sqlight.text(ingredient.name), 37 + sqlight.text(ingredient.unit), 38 + sqlight.int(ingredient.quantity), 39 + ] 40 + }) 41 + 42 + sqlight.query(sql, on: conn, with: args, expecting: read.single_col_decode()) 43 + } 44 + 45 + /// Takes a NewRecipe and writes to the db. This is effectively the meta data consisting of the recipe title, author, servings, and description. 46 + /// 47 + pub fn write_recipe( 48 + recipe: t.NewRecipe, 49 + conn: sqlight.Connection, 50 + ) -> Result(List(#(Int, String, Int, Int, String)), sqlight.Error) { 51 + write_single_column([recipe.author], t.Authors, conn) 52 + 53 + let sql = 54 + " 55 + insert into recipes (title, author_id, servings, description) 56 + values 57 + (?, 58 + (select id from authors where name = ?), 59 + ?, 60 + ?) 61 + " 62 + sqlight.query( 63 + sql, 64 + on: conn, 65 + with: [ 66 + sqlight.text(recipe.title), 67 + sqlight.text(recipe.author), 68 + sqlight.int(recipe.servings), 69 + sqlight.text(recipe.description), 70 + ], 71 + expecting: read.recipe_decode(), 72 + ) 73 + } 74 + 75 + /// Writes a row in one of the single-column tables: authors, units, or ingredients. Will ignore duplicates. Takes a list of strings. 76 + /// 77 + pub fn write_single_column( 78 + names: List(String), 79 + table: t.SingleColumnTable, 80 + conn: sqlight.Connection, 81 + ) { 82 + let table = case table { 83 + t.Authors -> "authors" 84 + t.Units -> "units" 85 + t.Ingredients -> "ingredients" 86 + } 87 + let sql = " 88 + INSERT OR IGNORE INTO " <> table <> " ('name') VALUES " <> list.repeat( 89 + "(?)", 90 + list.length(names), 91 + ) 92 + |> string.join(",") 93 + 94 + let args = 95 + list.flat_map(names, fn(name) { 96 + [ 97 + sqlight.text(name), 98 + ] 99 + }) 100 + sqlight.query(sql, on: conn, with: args, expecting: read.single_col_decode()) 101 + } 102 + 103 + pub fn write_instructions( 104 + instructions: List(t.Instruction), 105 + recipe_id: Int, 106 + conn: sqlight.Connection, 107 + ) { 108 + let sql = 109 + "INSERT INTO recipe_instructions (recipe_id, step, instruction) VALUES " 110 + <> list.repeat("(?,?,?)", list.length(instructions)) 111 + |> string.join(",") 112 + 113 + let args = 114 + list.flat_map(instructions, fn(instruction) { 115 + [ 116 + sqlight.int(recipe_id), 117 + sqlight.int(instruction.step), 118 + sqlight.text(instruction.instruction), 119 + ] 120 + }) 121 + 122 + sqlight.query(sql, on: conn, with: args, expecting: read.instruction_decode()) 123 + } 124 + 125 + pub fn write_full_recipe(recipe: t.RecipeDetailView, conn: sqlight.Connection) { 126 + let core_recipe = 127 + t.NewRecipe( 128 + title: recipe.title, 129 + author: recipe.author, 130 + servings: recipe.servings, 131 + description: recipe.description, 132 + ) 133 + write_recipe(core_recipe, conn) 134 + 135 + let assert Ok([row_id]) = 136 + sqlight.query( 137 + "SELECT last_insert_rowid()", 138 + on: conn, 139 + with: [], 140 + expecting: read.decode_int(), 141 + ) 142 + write_recipe_ingredients(recipe.ingredients, row_id, conn) 143 + write_instructions(recipe.instructions, row_id, conn) 144 + } 145 + 146 + pub fn optional_db_setup(conn: sqlight.Connection) -> Nil { 147 + let assert Ok(schema) = simplifile.read("./data/test_db.sql") 148 + // test recipe is "Soup" 149 + let _ = sqlight.exec(schema, conn) 150 + let _ = read.get_full_recipe("Soup", conn) 151 + let new_recipe = 152 + t.RecipeDetailView( 153 + title: "pizza", 154 + author: "dean martin", 155 + servings: 8, 156 + description: "oh my thats a lovely pizza", 157 + ingredients: [ 158 + t.Ingredient(name: "pepperoni", unit: "oz", quantity: 8), 159 + t.Ingredient(name: "tomato", unit: "cans", quantity: 2), 160 + t.Ingredient(name: "cheez", unit: "g", quantity: 54), 161 + ], 162 + instructions: [ 163 + t.Instruction(step: 1, instruction: "make-a the dough"), 164 + t.Instruction(step: 2, instruction: "make-a the sauce"), 165 + t.Instruction(step: 3, instruction: "make-a the pizza"), 166 + ], 167 + ) 168 + let _ = write_full_recipe(new_recipe, conn) 169 + let _ = read.get_full_recipe("pizza", conn) 170 + Nil 171 + }
+92
server/src/server.gleam
··· 1 + import gleam/dynamic/decode 2 + import gleam/http/response 3 + import gleam/string 4 + import queries/read 5 + import queries/write 6 + import shared/types as t 7 + import sqlight 8 + 9 + import gleam/erlang/process 10 + import gleam/http.{Get, Post} 11 + import gleam/json 12 + import gleam/result 13 + import lustre/attribute 14 + import lustre/element 15 + import lustre/element/html 16 + import mist 17 + import wisp.{type Request, type Response} 18 + import wisp/wisp_mist 19 + 20 + pub fn main() { 21 + wisp.configure_logger() 22 + let base_key = wisp.random_string(64) 23 + 24 + let assert Ok(conn) = sqlight.open("data/db.db") 25 + 26 + let assert Ok(priv_directory) = wisp.priv_directory("server") 27 + let static_directory = priv_directory <> "/static" 28 + 29 + let assert Ok(_) = 30 + handle_request(conn, static_directory, _) 31 + |> wisp_mist.handler(base_key) 32 + |> mist.new 33 + |> mist.port(3000) 34 + |> mist.start 35 + 36 + process.sleep_forever() 37 + } 38 + 39 + fn app_middleware( 40 + req: Request, 41 + static_directory: String, 42 + next: fn(Request) -> Response, 43 + ) -> Response { 44 + let req = wisp.method_override(req) 45 + use <- wisp.log_request(req) 46 + use <- wisp.rescue_crashes 47 + use req <- wisp.handle_head(req) 48 + use <- wisp.serve_static(req, under: "/static", from: static_directory) 49 + 50 + next(req) 51 + } 52 + 53 + fn handle_request( 54 + db: sqlight.Connection, 55 + static_directory: String, 56 + req: Request, 57 + ) -> Response { 58 + use req <- app_middleware(req, static_directory) 59 + 60 + case wisp.path_segments(req) { 61 + ["api", "list"] -> show_list(db) 62 + [title] -> show_recipe(db, title) 63 + [] -> show_home(db) 64 + _ -> wisp.not_found() 65 + } 66 + } 67 + 68 + fn show_recipe( 69 + db: sqlight.Connection, 70 + title: String, 71 + ) -> response.Response(wisp.Body) { 72 + let string = 73 + read.get_full_recipe(title, db) 74 + |> string.inspect 75 + |> echo 76 + wisp.ok() 77 + |> wisp.html_body(string) 78 + } 79 + 80 + fn show_home(db: sqlight.Connection) -> response.Response(wisp.Body) { 81 + wisp.ok() 82 + |> wisp.html_body("homepage") 83 + } 84 + 85 + fn show_list(db: sqlight.Connection) -> response.Response(wisp.Body) { 86 + let list = 87 + read.get_indexed_recipes(db) 88 + |> string.inspect 89 + 90 + wisp.ok() 91 + |> wisp.html_body(list) 92 + }
+170
server/test/server_test.gleam
··· 1 + import birdie 2 + import gleam/string 3 + import gleeunit 4 + import queries/read 5 + import queries/write 6 + import shared/types as t 7 + import simplifile 8 + import sqlight 9 + 10 + pub fn main() -> Nil { 11 + gleeunit.main() 12 + } 13 + 14 + pub fn write_schema_test() { 15 + use conn <- sqlight.with_connection(":memory:") 16 + 17 + let assert Ok(schema) = simplifile.read("./data/seed.sql") 18 + let assert Ok(Nil) = sqlight.exec(schema, conn) 19 + } 20 + 21 + pub fn write_single_column_test() { 22 + use conn <- sqlight.with_connection(":memory:") 23 + let assert Ok(schema) = simplifile.read("./data/seed.sql") 24 + let _ = sqlight.exec(schema, conn) 25 + let assert Ok([]) = write.write_single_column(["mom"], t.Authors, conn) 26 + } 27 + 28 + pub fn write_recipe_test() { 29 + use conn <- sqlight.with_connection(":memory:") 30 + let assert Ok(schema) = simplifile.read("./data/seed.sql") 31 + let _ = sqlight.exec(schema, conn) 32 + 33 + let soup = 34 + t.NewRecipe( 35 + title: "soup", 36 + author: "mom", 37 + servings: 3, 38 + description: "its-a soup!", 39 + ) 40 + 41 + let assert Ok([]) = write.write_recipe(soup, conn) 42 + } 43 + 44 + pub fn write_ingredient_test() { 45 + use conn <- sqlight.with_connection(":memory:") 46 + let assert Ok(schema) = simplifile.read("./data/seed.sql") 47 + let _ = sqlight.exec(schema, conn) 48 + 49 + let soup = 50 + t.NewRecipe( 51 + title: "soup", 52 + author: "mom", 53 + servings: 3, 54 + description: "its-a soup!", 55 + ) 56 + 57 + let _ = write.write_recipe(soup, conn) 58 + let ingredients = [ 59 + t.Ingredient(name: "onion", unit: "lb", quantity: 7), 60 + t.Ingredient(name: "water", unit: "cup", quantity: 1), 61 + t.Ingredient(name: "tarragon", unit: "tbsp", quantity: 69), 62 + ] 63 + 64 + let assert Ok([]) = write.write_recipe_ingredients(ingredients, 1, conn) 65 + } 66 + 67 + pub fn write_instruction_test() { 68 + use conn <- sqlight.with_connection(":memory:") 69 + let assert Ok(schema) = simplifile.read("./data/seed.sql") 70 + let _ = sqlight.exec(schema, conn) 71 + 72 + let soup = 73 + t.NewRecipe( 74 + title: "soup", 75 + author: "mom", 76 + servings: 3, 77 + description: "its-a soup!", 78 + ) 79 + 80 + let _ = write.write_recipe(soup, conn) 81 + let instructions = [ 82 + t.Instruction(1, "chop"), 83 + t.Instruction(2, "fry"), 84 + t.Instruction(3, "bake"), 85 + ] 86 + 87 + let assert Ok([]) = write.write_instructions(instructions, 1, conn) 88 + } 89 + 90 + pub fn test_db_population_test() { 91 + use conn <- sqlight.with_connection(":memory:") 92 + let assert Ok(schema) = simplifile.read("./data/test_db.sql") 93 + let assert Ok(Nil) = sqlight.exec(schema, conn) 94 + } 95 + 96 + pub fn recipe_read_test() { 97 + use conn <- sqlight.with_connection(":memory:") 98 + let assert Ok(schema) = simplifile.read("./data/test_db.sql") 99 + let _ = sqlight.exec(schema, conn) 100 + sqlight.query("select * from recipes", conn, [], read.recipe_decode()) 101 + |> string.inspect() 102 + |> birdie.snap(title: "read recipe table") 103 + } 104 + 105 + pub fn recipe_ingredients_read_test() { 106 + use conn <- sqlight.with_connection(":memory:") 107 + let assert Ok(schema) = simplifile.read("./data/test_db.sql") 108 + let _ = sqlight.exec(schema, conn) 109 + sqlight.query( 110 + "select * from recipe_ingredients", 111 + conn, 112 + [], 113 + read.recipe_ingredient_decode(), 114 + ) 115 + |> string.inspect() 116 + |> birdie.snap(title: "read recipe_ingredients table") 117 + } 118 + 119 + pub fn read_full_recipe_test() { 120 + use conn <- sqlight.with_connection(":memory:") 121 + let assert Ok(schema) = simplifile.read("./data/test_db.sql") 122 + let _ = sqlight.exec(schema, conn) 123 + read.get_full_recipe("Soup", conn) 124 + |> string.inspect() 125 + |> birdie.snap(title: "full reciple read") 126 + } 127 + 128 + pub fn get_indexed_recipes_test() { 129 + use conn <- sqlight.with_connection(":memory:") 130 + let assert Ok(schema) = simplifile.read("./data/test_db.sql") 131 + let _ = sqlight.exec(schema, conn) 132 + let pie = 133 + t.NewRecipe( 134 + title: "pie", 135 + author: "mom", 136 + servings: 3, 137 + description: "delicious pie", 138 + ) 139 + 140 + let _ = write.write_recipe(pie, conn) 141 + read.get_indexed_recipes(conn) 142 + |> string.inspect() 143 + |> birdie.snap(title: "indexed recipes") 144 + } 145 + 146 + pub fn full_recipe_write_test() { 147 + use conn <- sqlight.with_connection(":memory:") 148 + let assert Ok(schema) = simplifile.read("./data/seed.sql") 149 + let _ = sqlight.exec(schema, conn) 150 + 151 + let new_recipe = 152 + t.RecipeDetailView( 153 + title: "pizza", 154 + author: "dean martin", 155 + servings: 8, 156 + description: "oh my thats a lovely pizza", 157 + ingredients: [ 158 + t.Ingredient(name: "pepperoni", unit: "oz", quantity: 8), 159 + t.Ingredient(name: "tomato", unit: "cans", quantity: 2), 160 + t.Ingredient(name: "cheez", unit: "g", quantity: 54), 161 + ], 162 + instructions: [ 163 + t.Instruction(step: 1, instruction: "make-a the dough"), 164 + t.Instruction(step: 2, instruction: "make-a the sauce"), 165 + t.Instruction(step: 3, instruction: "make-a the pizza"), 166 + ], 167 + ) 168 + 169 + let assert Ok([]) = write.write_full_recipe(new_recipe, conn) 170 + }
+24
shared/README.md
··· 1 + # shared 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/shared)](https://hex.pm/packages/shared) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/shared/) 5 + 6 + ```sh 7 + gleam add shared@1 8 + ``` 9 + ```gleam 10 + import shared 11 + 12 + pub fn main() -> Nil { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/shared>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+20
shared/gleam.toml
··· 1 + name = "shared" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "", repo = "" } 10 + # links = [{ title = "Website", href = "" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 + gleam_json = ">= 3.1.0 and < 4.0.0" 18 + 19 + [dev-dependencies] 20 + gleeunit = ">= 1.0.0 and < 2.0.0"
+13
shared/manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 6 + { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 7 + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 8 + ] 9 + 10 + [requirements] 11 + gleam_json = { version = ">= 3.1.0 and < 4.0.0" } 12 + gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 13 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+127
shared/src/shared/types.gleam
··· 1 + import gleam/dynamic/decode 2 + import gleam/json 3 + 4 + pub type NewRecipe { 5 + NewRecipe(title: String, author: String, servings: Int, description: String) 6 + } 7 + 8 + pub type RecipeRow { 9 + RecipeRow( 10 + id: Int, 11 + title: String, 12 + author: String, 13 + servings: Int, 14 + description: String, 15 + ) 16 + } 17 + 18 + pub type RecipeDetailView { 19 + RecipeDetailView( 20 + title: String, 21 + author: String, 22 + servings: Int, 23 + description: String, 24 + ingredients: List(Ingredient), 25 + instructions: List(Instruction), 26 + ) 27 + } 28 + 29 + pub type RecipeStub { 30 + RecipeStub(recipe_id: Int, title: String) 31 + } 32 + 33 + pub type Ingredient { 34 + Ingredient(name: String, unit: String, quantity: Int) 35 + } 36 + 37 + pub type Instruction { 38 + Instruction(step: Int, instruction: String) 39 + } 40 + 41 + pub type SingleColumnTable { 42 + Authors 43 + Ingredients 44 + Units 45 + } 46 + 47 + fn recipe_detail_view_to_json(recipe_detail_view: RecipeDetailView) -> json.Json { 48 + let RecipeDetailView( 49 + title:, 50 + author:, 51 + servings:, 52 + description:, 53 + ingredients:, 54 + instructions:, 55 + ) = recipe_detail_view 56 + json.object([ 57 + #("title", json.string(title)), 58 + #("author", json.string(author)), 59 + #("servings", json.int(servings)), 60 + #("description", json.string(description)), 61 + #("ingredients", json.array(ingredients, ingredient_to_json)), 62 + #("instructions", json.array(instructions, instruction_to_json)), 63 + ]) 64 + } 65 + 66 + pub fn ingredient_to_json(ingredient: Ingredient) -> json.Json { 67 + let Ingredient(name:, unit:, quantity:) = ingredient 68 + json.object([ 69 + #("name", json.string(name)), 70 + #("unit", json.string(unit)), 71 + #("quantity", json.int(quantity)), 72 + ]) 73 + } 74 + 75 + pub fn instruction_to_json(instruction: Instruction) -> json.Json { 76 + let Instruction(step:, instruction:) = instruction 77 + json.object([ 78 + #("step", json.int(step)), 79 + #("instruction", json.string(instruction)), 80 + ]) 81 + } 82 + 83 + pub fn new_recipe_to_json(new_recipe: NewRecipe) -> json.Json { 84 + let NewRecipe(title:, author:, servings:, description:) = new_recipe 85 + json.object([ 86 + #("title", json.string(title)), 87 + #("author", json.string(author)), 88 + #("servings", json.int(servings)), 89 + #("description", json.string(description)), 90 + ]) 91 + } 92 + 93 + pub fn recipe_detail_view_decoder() -> decode.Decoder(RecipeDetailView) { 94 + use title <- decode.field("title", decode.string) 95 + use author <- decode.field("author", decode.string) 96 + use servings <- decode.field("servings", decode.int) 97 + use description <- decode.field("description", decode.string) 98 + use ingredients <- decode.field( 99 + "ingredients", 100 + decode.list(ingredient_decoder()), 101 + ) 102 + use instructions <- decode.field( 103 + "instructions", 104 + decode.list(instruction_decoder()), 105 + ) 106 + decode.success(RecipeDetailView( 107 + title:, 108 + author:, 109 + servings:, 110 + description:, 111 + ingredients:, 112 + instructions:, 113 + )) 114 + } 115 + 116 + pub fn instruction_decoder() -> decode.Decoder(Instruction) { 117 + use step <- decode.field("step", decode.int) 118 + use instruction <- decode.field("instruction", decode.string) 119 + decode.success(Instruction(step:, instruction:)) 120 + } 121 + 122 + pub fn ingredient_decoder() -> decode.Decoder(Ingredient) { 123 + use name <- decode.field("name", decode.string) 124 + use unit <- decode.field("unit", decode.string) 125 + use quantity <- decode.field("quantity", decode.int) 126 + decode.success(Ingredient(name:, unit:, quantity:)) 127 + }
+13
shared/test/shared_test.gleam
··· 1 + import gleeunit 2 + 3 + pub fn main() -> Nil { 4 + gleeunit.main() 5 + } 6 + 7 + // gleeunit test functions end in `_test` 8 + pub fn hello_world_test() { 9 + let name = "Joe" 10 + let greeting = "Hello, " <> name <> "!" 11 + 12 + assert greeting == "Hello, Joe!" 13 + }