Trading card city builder game?

install squirrel for database accessor generation

+318 -468
+11 -15
.github/workflows/sql.yml
··· 7 7 ROOT_DATABASE_URL: postgresql://postgres:postgres@localhost/postgres 8 8 9 9 jobs: 10 - status: 11 - runs-on: ubuntu-latest 12 - steps: 13 - - name: Checkout code 14 - uses: actions/checkout@v4 15 - - name: Install Node.js 16 - uses: actions/setup-node@v4 17 - with: 18 - cache: npm 19 - node-version-file: .node-version 20 - - name: Install node_modules 21 - run: npm ci 22 - - name: Run status 23 - run: npx graphile-migrate status --skipDatabase 24 - 25 10 migrate: 26 11 runs-on: ubuntu-latest 27 12 services: ··· 45 30 with: 46 31 cache: npm 47 32 node-version-file: .node-version 33 + - name: Set up Gleam 34 + uses: erlef/setup-beam@v1 35 + with: 36 + gleam-version: "1.14.0" 37 + otp-version: "27" 38 + rebar3-version: "3.25.1" 48 39 - name: Install node_modules 49 40 run: npm ci 41 + - name: Check status 42 + run: npx graphile-migrate status --skipDatabase 50 43 - name: Run migrate 51 44 run: npx graphile-migrate migrate 45 + - name: Check squirrel 46 + run: gleam run -m squirrel check 47 + working-directory: ./server
+6 -1
Justfile
··· 19 19 shadow_database_name := if SHADOW_DATABASE_URL != "" { file_stem(SHADOW_DATABASE_URL) } else { "" } 20 20 21 21 [group: "run"] 22 - dev: up 22 + dev: up squirrel 23 23 npx concurrently --names "sveltekit,migrate,server" \ 24 24 "npx vite dev --host" \ 25 25 "npx graphile-migrate watch" \ ··· 103 103 [group: "database"] 104 104 migrate: 105 105 npx graphile-migrate migrate --forceActions 106 + 107 + [group: "database"] 108 + [working-directory: "server"] 109 + squirrel: up migrate 110 + gleam run -m squirrel 106 111 107 112 [group: "database"] 108 113 migration-dev:
-23
server/.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
+1
server/gleam.toml
··· 23 23 gleam_http = ">= 4.3.0 and < 5.0.0" 24 24 gleam_time = ">= 1.6.0 and < 2.0.0" 25 25 palabres = ">= 1.0.3 and < 2.0.0" 26 + squirrel = ">= 4.6.0 and < 5.0.0" 26 27 27 28 [dev-dependencies] 28 29 gleeunit = ">= 1.0.0 and < 2.0.0"
+19
server/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 5 6 { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, 6 7 { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 8 + { name = "eval", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "eval", source = "hex", outer_checksum = "264DAF4B49DF807F303CA4A4E4EBC012070429E40BE384C58FE094C4958F9BDA" }, 7 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 = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, 12 + { 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" }, 13 + { 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" }, 8 14 { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 9 15 { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 10 16 { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 11 17 { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 12 18 { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 19 + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 13 20 { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 14 21 { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, 15 22 { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 16 23 { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 24 + { name = "glexer", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "40A1FB0919FA080AD6C5809B4C7DBA545841CAAC8168FACDFA0B0667C22475CC" }, 17 25 { 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" }, 18 26 { 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" }, 19 27 { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 28 + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 20 29 { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 21 30 { 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" }, 31 + { name = "mug", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "mug", source = "hex", outer_checksum = "C01279D98E40371DA23461774B63F0E3581B8F1396049D881B0C7EB32799D93F" }, 32 + { name = "non_empty_list", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "non_empty_list", source = "hex", outer_checksum = "1CA43D18C07E98E9ED5A60D9CB2FFE0FF40DEFFA45D58A3FF589589F05658F7B" }, 22 33 { name = "opentelemetry_api", version = "1.5.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "F53EC8A1337AE4A487D43AC89DA4BD3A3C99DDF576655D071DEED8B56A2D5DDA" }, 23 34 { name = "palabres", version = "1.0.4", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib"], otp_app = "palabres", source = "hex", outer_checksum = "2809B7C10AE929E82454B4A6A0FB7D02073C5A509F9A63EE9F7B268A75D98965" }, 24 35 { name = "pg_types", version = "0.6.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "9949A4849DD13408FA249AB7B745E0D2DFDB9532AEE2B9722326E33CD082A778" }, 25 36 { name = "pgo", version = "0.20.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "2F11E6649CEB38E569EF56B16BE1D04874AE5B11A02867080A2817CE423C683B" }, 26 37 { name = "pog", version = "4.1.0", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_otp", "gleam_stdlib", "gleam_time", "pgo"], source = "git", repo = "https://github.com/foxfriends/pog.git", commit = "20bf4222c9914ca98fa897a0eb5bcbf8968a5061" }, 38 + { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, 39 + { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, 40 + { name = "squirrel", version = "4.6.0", build_tools = ["gleam"], requirements = ["argv", "envoy", "eval", "filepath", "glam", "gleam_community_ansi", "gleam_crypto", "gleam_json", "gleam_regexp", "gleam_stdlib", "gleam_time", "glexer", "justin", "mug", "non_empty_list", "pog", "simplifile", "term_size", "tom", "tote", "youid"], otp_app = "squirrel", source = "hex", outer_checksum = "0ED10A868BDD1A5D4B68D99CD1C72DC3F23C6E36E16D33454C5F0C31BAC9CB1E" }, 27 41 { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 42 + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, 43 + { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, 44 + { name = "tote", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tote", source = "hex", outer_checksum = "A249892E26A53C668897F8D47845B0007EEE07707A1A03437487F0CD5A452CA5" }, 45 + { name = "youid", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_stdlib", "gleam_time"], otp_app = "youid", source = "hex", outer_checksum = "580E909FD704DB16416D5CB080618EDC2DA0F1BE4D21B490C0683335E3FFC5AF" }, 28 46 ] 29 47 30 48 [requirements] ··· 39 57 mist = { version = ">= 5.0.3 and < 6.0.0" } 40 58 palabres = { version = ">= 1.0.3 and < 2.0.0" } 41 59 pog = { git = "https://github.com/foxfriends/pog.git", ref = "20bf422" } 60 + squirrel = { version = ">= 4.6.0 and < 5.0.0" }
+208
server/src/db/sql.gleam
··· 1 + //// This module contains the code to run the sql queries defined in 2 + //// `./src/db/sql`. 3 + //// > 🐿️ This module was generated automatically using v4.6.0 of 4 + //// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 5 + //// 6 + 7 + import gleam/dynamic/decode 8 + import pog 9 + 10 + /// A row you get from running the `create_account` query 11 + /// defined in `./src/db/sql/create_account.sql`. 12 + /// 13 + /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 14 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 15 + /// 16 + pub type CreateAccountRow { 17 + CreateAccountRow(id: String) 18 + } 19 + 20 + /// Runs the `create_account` query 21 + /// defined in `./src/db/sql/create_account.sql`. 22 + /// 23 + /// > 🐿️ This function was generated automatically using v4.6.0 of 24 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 25 + /// 26 + pub fn create_account( 27 + db: pog.Connection, 28 + arg_1: String, 29 + ) -> Result(pog.Returned(CreateAccountRow), pog.QueryError) { 30 + let decoder = { 31 + use id <- decode.field(0, decode.string) 32 + decode.success(CreateAccountRow(id:)) 33 + } 34 + 35 + "INSERT INTO 36 + accounts (id) 37 + VALUES 38 + ($1) 39 + ON CONFLICT DO NOTHING 40 + RETURNING 41 + * 42 + " 43 + |> pog.query 44 + |> pog.parameter(pog.text(arg_1)) 45 + |> pog.returning(decoder) 46 + |> pog.execute(db) 47 + } 48 + 49 + /// A row you get from running the `get_account` query 50 + /// defined in `./src/db/sql/get_account.sql`. 51 + /// 52 + /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 53 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 54 + /// 55 + pub type GetAccountRow { 56 + GetAccountRow(id: String) 57 + } 58 + 59 + /// Runs the `get_account` query 60 + /// defined in `./src/db/sql/get_account.sql`. 61 + /// 62 + /// > 🐿️ This function was generated automatically using v4.6.0 of 63 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 64 + /// 65 + pub fn get_account( 66 + db: pog.Connection, 67 + arg_1: String, 68 + ) -> Result(pog.Returned(GetAccountRow), pog.QueryError) { 69 + let decoder = { 70 + use id <- decode.field(0, decode.string) 71 + decode.success(GetAccountRow(id:)) 72 + } 73 + 74 + "SELECT 75 + * 76 + FROM 77 + accounts 78 + WHERE 79 + id = $1 80 + " 81 + |> pog.query 82 + |> pog.parameter(pog.text(arg_1)) 83 + |> pog.returning(decoder) 84 + |> pog.execute(db) 85 + } 86 + 87 + /// A row you get from running the `get_field_and_tiles_by_id` query 88 + /// defined in `./src/db/sql/get_field_and_tiles_by_id.sql`. 89 + /// 90 + /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 91 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 92 + /// 93 + pub type GetFieldAndTilesByIdRow { 94 + GetFieldAndTilesByIdRow(field: String, field_tiles: String) 95 + } 96 + 97 + /// Runs the `get_field_and_tiles_by_id` query 98 + /// defined in `./src/db/sql/get_field_and_tiles_by_id.sql`. 99 + /// 100 + /// > 🐿️ This function was generated automatically using v4.6.0 of 101 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 102 + /// 103 + pub fn get_field_and_tiles_by_id( 104 + db: pog.Connection, 105 + arg_1: Int, 106 + ) -> Result(pog.Returned(GetFieldAndTilesByIdRow), pog.QueryError) { 107 + let decoder = { 108 + use field <- decode.field(0, decode.string) 109 + use field_tiles <- decode.field(1, decode.string) 110 + decode.success(GetFieldAndTilesByIdRow(field:, field_tiles:)) 111 + } 112 + 113 + "SELECT 114 + to_json(fields.*) AS field, 115 + json_arrayagg (field_tiles.* ABSENT ON NULL) AS field_tiles 116 + FROM 117 + fields 118 + LEFT JOIN field_tiles ON field_tiles.field_id = fields.id 119 + WHERE 120 + fields.id = $1 121 + GROUP BY 122 + fields.id 123 + " 124 + |> pog.query 125 + |> pog.parameter(pog.int(arg_1)) 126 + |> pog.returning(decoder) 127 + |> pog.execute(db) 128 + } 129 + 130 + /// A row you get from running the `get_field_by_id` query 131 + /// defined in `./src/db/sql/get_field_by_id.sql`. 132 + /// 133 + /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 134 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 135 + /// 136 + pub type GetFieldByIdRow { 137 + GetFieldByIdRow(id: Int, name: String, account_id: String) 138 + } 139 + 140 + /// Runs the `get_field_by_id` query 141 + /// defined in `./src/db/sql/get_field_by_id.sql`. 142 + /// 143 + /// > 🐿️ This function was generated automatically using v4.6.0 of 144 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 145 + /// 146 + pub fn get_field_by_id( 147 + db: pog.Connection, 148 + arg_1: Int, 149 + ) -> Result(pog.Returned(GetFieldByIdRow), pog.QueryError) { 150 + let decoder = { 151 + use id <- decode.field(0, decode.int) 152 + use name <- decode.field(1, decode.string) 153 + use account_id <- decode.field(2, decode.string) 154 + decode.success(GetFieldByIdRow(id:, name:, account_id:)) 155 + } 156 + 157 + "SELECT 158 + * 159 + FROM 160 + fields 161 + WHERE 162 + id = $1 163 + " 164 + |> pog.query 165 + |> pog.parameter(pog.int(arg_1)) 166 + |> pog.returning(decoder) 167 + |> pog.execute(db) 168 + } 169 + 170 + /// A row you get from running the `list_fields_for_account` query 171 + /// defined in `./src/db/sql/list_fields_for_account.sql`. 172 + /// 173 + /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 174 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 175 + /// 176 + pub type ListFieldsForAccountRow { 177 + ListFieldsForAccountRow(id: Int, name: String, account_id: String) 178 + } 179 + 180 + /// Runs the `list_fields_for_account` query 181 + /// defined in `./src/db/sql/list_fields_for_account.sql`. 182 + /// 183 + /// > 🐿️ This function was generated automatically using v4.6.0 of 184 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 185 + /// 186 + pub fn list_fields_for_account( 187 + db: pog.Connection, 188 + arg_1: String, 189 + ) -> Result(pog.Returned(ListFieldsForAccountRow), pog.QueryError) { 190 + let decoder = { 191 + use id <- decode.field(0, decode.int) 192 + use name <- decode.field(1, decode.string) 193 + use account_id <- decode.field(2, decode.string) 194 + decode.success(ListFieldsForAccountRow(id:, name:, account_id:)) 195 + } 196 + 197 + "SELECT 198 + * 199 + FROM 200 + fields 201 + WHERE 202 + account_id = $1 203 + " 204 + |> pog.query 205 + |> pog.parameter(pog.text(arg_1)) 206 + |> pog.returning(decoder) 207 + |> pog.execute(db) 208 + }
+7
server/src/db/sql/create_account.sql
··· 1 + INSERT INTO 2 + accounts (id) 3 + VALUES 4 + ($1) 5 + ON CONFLICT DO NOTHING 6 + RETURNING 7 + *
+6
server/src/db/sql/get_account.sql
··· 1 + SELECT 2 + * 3 + FROM 4 + accounts 5 + WHERE 6 + id = $1
+10
server/src/db/sql/get_field_and_tiles_by_id.sql
··· 1 + SELECT 2 + to_json(fields.*) AS field, 3 + json_arrayagg (field_tiles.* ABSENT ON NULL) AS field_tiles 4 + FROM 5 + fields 6 + LEFT JOIN field_tiles ON field_tiles.field_id = fields.id 7 + WHERE 8 + fields.id = $1 9 + GROUP BY 10 + fields.id
+6
server/src/db/sql/get_field_by_id.sql
··· 1 + SELECT 2 + * 3 + FROM 4 + fields 5 + WHERE 6 + id = $1
+6
server/src/db/sql/list_fields_for_account.sql
··· 1 + SELECT 2 + * 3 + FROM 4 + fields 5 + WHERE 6 + account_id = $1
+1 -8
server/src/dto/channel.gleam
··· 1 1 import gleam/dynamic/decode 2 2 3 3 pub type Channel { 4 - Fields 5 - FieldTiles(Int) 6 4 Deck 7 5 } 8 6 ··· 13 11 ]), 14 12 ) 15 13 case name { 16 - "fields" -> decode.success(Fields) 17 - "field_tiles" -> { 18 - use field_id <- decode.subfield(["channel", "field_id"], decode.int) 19 - decode.success(FieldTiles(field_id)) 20 - } 21 14 "deck" -> decode.success(Deck) 22 - _ -> decode.failure(Fields, "valid channel") 15 + _ -> decode.failure(Deck, "valid channel") 23 16 } 24 17 }
+7 -16
server/src/handlers/auth_handler.gleam
··· 1 1 import db/rows 2 + import db/sql 2 3 import dto/output_action 3 4 import dto/output_message 4 5 import gleam/option ··· 6 7 import gleam/string 7 8 import mist 8 9 import models/account 9 - import pog 10 10 import websocket/state 11 11 12 12 pub fn handle( ··· 15 15 message_id: String, 16 16 account_id: String, 17 17 ) -> Result(mist.Next(state.State, _msg), String) { 18 - let assert Ok(acc) = 19 - pog.query( 20 - "INSERT INTO accounts (id) VALUES ($1) ON CONFLICT DO NOTHING RETURNING *", 21 - ) 22 - |> pog.parameter(pog.text(account_id)) 23 - |> pog.returning(account.from_sql_row()) 24 - |> pog.execute(state.db_connection(st)) 25 18 { 19 + let assert Ok(acc) = sql.create_account(state.db_connection(st), account_id) 26 20 use acc <- rows.one_or_none(acc) 27 - let assert Ok(acc) = case acc { 28 - option.Some(acc) -> Ok(acc) 21 + let assert Ok(account_id) = case acc { 22 + option.Some(acc) -> Ok(acc.id) 29 23 option.None -> { 30 24 let assert Ok(acc) = 31 - pog.query("SELECT * FROM accounts WHERE id = $1") 32 - |> pog.parameter(pog.text(account_id)) 33 - |> pog.returning(account.from_sql_row()) 34 - |> pog.execute(state.db_connection(st)) 25 + sql.get_account(state.db_connection(st), account_id) 35 26 use acc <- rows.one(acc) 36 - Ok(acc) 27 + Ok(acc.id) 37 28 } 38 29 } 39 30 40 31 use _ <- result.map( 41 - output_action.Account(account.Account(id: acc.id)) 32 + output_action.Account(account.Account(id: account_id)) 42 33 |> output_message.OutputMessage(id: message_id) 43 34 |> output_message.send(conn) 44 35 |> result.map_error(rows.HandlerError),
+10 -23
server/src/handlers/get_field_handler.gleam
··· 1 1 import db/rows 2 + import db/sql 2 3 import dto/output_action 3 4 import dto/output_message 4 5 import gleam/dynamic/decode 6 + import gleam/json 5 7 import gleam/result 6 8 import gleam/string 7 9 import mist 8 10 import models/field 9 11 import models/field_tile 10 - import pog 11 12 import websocket/state 12 13 13 14 pub fn handle( ··· 16 17 message_id: String, 17 18 field_id: Int, 18 19 ) { 19 - let assert Ok(result) = 20 - pog.query( 21 - "SELECT 22 - to_json(fields.*) AS field, 23 - json_arrayagg(field_tiles.* ABSENT ON NULL) AS field_tiles 24 - FROM fields 25 - LEFT JOIN field_tiles ON field_tiles.field_id = fields.id 26 - WHERE fields.id = $1 27 - GROUP BY fields.id", 20 + { 21 + let assert Ok(result) = 22 + sql.get_field_and_tiles_by_id(state.db_connection(st), field_id) 23 + use sql.GetFieldAndTilesByIdRow(field_json, field_tiles_json) <- rows.one( 24 + result, 28 25 ) 29 - |> pog.parameter(pog.int(field_id)) 30 - |> pog.returning({ 31 - use field_data <- decode.field(0, rows.json(field.from_json())) 32 - use field_cards_data <- decode.field( 33 - 1, 34 - rows.json(decode.list(field_tile.from_json())), 35 - ) 36 - decode.success(#(field_data, field_cards_data)) 37 - }) 38 - |> pog.execute(state.db_connection(st)) 39 - { 40 - use #(field_data, field_tiles) <- rows.one(result) 41 - 26 + let assert Ok(field_data) = json.parse(field_json, field.from_json()) 27 + let assert Ok(field_tiles) = 28 + json.parse(field_tiles_json, decode.list(field_tile.from_json())) 42 29 use _ <- result.map( 43 30 output_action.FieldWithTiles(field: field_data, field_tiles:) 44 31 |> output_message.OutputMessage(id: message_id)
+6 -7
server/src/handlers/get_fields_handler.gleam
··· 1 + import db/sql 1 2 import dto/output_action 2 3 import dto/output_message 4 + import gleam/list 3 5 import mist 4 6 import models/field 5 - import pog 6 7 import websocket/state 7 8 8 9 pub fn handle( ··· 12 13 ) -> Result(mist.Next(state.State, _msg), String) { 13 14 use account_id <- state.account_id(st) 14 15 let assert Ok(result) = 15 - pog.query("SELECT * FROM fields WHERE account_id = $1") 16 - |> pog.parameter(pog.text(account_id)) 17 - |> pog.returning(field.from_sql_row()) 18 - |> pog.execute(state.db_connection(st)) 19 - 16 + sql.list_fields_for_account(state.db_connection(st), account_id) 20 17 let assert Ok(_) = 21 - output_action.Fields(result.rows) 18 + result.rows 19 + |> list.map(field.from_list_fields_for_account) 20 + |> output_action.Fields() 22 21 |> output_message.OutputMessage(id: message_id) 23 22 |> output_message.send(conn) 24 23
-55
server/src/handlers/listeners/card_accounts_listener.gleam
··· 1 - import db/notification_listener 2 - import dto/output_action 3 - import dto/output_message 4 - import gleam/dynamic/decode 5 - import gleam/string 6 - import mist 7 - import palabres 8 - import websocket/state.{type State as WebsocketState} 9 - 10 - type State { 11 - State(account_id: String, message_id: String, conn: mist.WebsocketConnection) 12 - } 13 - 14 - type Event { 15 - TransferCard(target: Int, subject: String) 16 - } 17 - 18 - fn event_decoder() { 19 - use event <- decode.field("event", decode.string) 20 - use target <- decode.field("target", decode.int) 21 - use subject <- decode.field("subject", decode.string) 22 - case event { 23 - "transfer_card" -> decode.success(TransferCard(target, subject)) 24 - _ -> decode.failure(TransferCard(target, subject), "card accounts event") 25 - } 26 - } 27 - 28 - pub fn start( 29 - st: WebsocketState, 30 - conn: mist.WebsocketConnection, 31 - account_id: String, 32 - message_id: String, 33 - ) { 34 - notification_listener.new(State(account_id:, conn:, message_id:)) 35 - |> notification_listener.listen_to( 36 - state.notifications_connection(st), 37 - "card_accounts:" <> account_id, 38 - ) 39 - |> notification_listener.on_notification(event_decoder(), on_notification) 40 - |> notification_listener.start() 41 - } 42 - 43 - fn on_notification(state: State, event: Event) { 44 - palabres.info("database card accounts event received") 45 - |> palabres.string("event", string.inspect(event)) 46 - |> palabres.log() 47 - 48 - let assert Ok(Nil) = case event { 49 - TransferCard(card_id, _) -> 50 - output_action.CardAccount(card_id) 51 - |> output_message.OutputMessage(state.message_id) 52 - |> output_message.send(state.conn) 53 - } 54 - notification_listener.continue(state) 55 - }
-90
server/src/handlers/listeners/field_cards_listener.gleam
··· 1 - import db/notification_listener 2 - import db/rows 3 - import dto/output_action 4 - import dto/output_message 5 - import gleam/dynamic/decode 6 - import gleam/int 7 - import gleam/result 8 - import gleam/string 9 - import mist 10 - import models/field_tile 11 - import palabres 12 - import pog 13 - import websocket/state.{type State as WebsocketState} 14 - 15 - type State { 16 - State( 17 - field_id: Int, 18 - message_id: String, 19 - conn: mist.WebsocketConnection, 20 - db: pog.Connection, 21 - ) 22 - } 23 - 24 - type Event { 25 - PlaceCard(target: Int, subject: String) 26 - UnplaceCard(target: Int, subject: String) 27 - } 28 - 29 - fn event_decoder() { 30 - use event <- decode.field("event", decode.string) 31 - use target <- decode.field("target", decode.int) 32 - use subject <- decode.field("subject", decode.string) 33 - case event { 34 - "place_card" -> decode.success(PlaceCard(target, subject)) 35 - "unplace_card" -> decode.success(UnplaceCard(target, subject)) 36 - _ -> decode.failure(PlaceCard(target, subject), "field cards event") 37 - } 38 - } 39 - 40 - pub fn start( 41 - st: WebsocketState, 42 - conn: mist.WebsocketConnection, 43 - field_id: Int, 44 - message_id: String, 45 - ) { 46 - notification_listener.new(State( 47 - field_id:, 48 - conn:, 49 - db: state.db_connection(st), 50 - message_id:, 51 - )) 52 - |> notification_listener.listen_to( 53 - state.notifications_connection(st), 54 - "field_tiles:" <> int.to_string(field_id), 55 - ) 56 - |> notification_listener.on_notification(event_decoder(), on_notification) 57 - |> notification_listener.start() 58 - } 59 - 60 - fn push_field_card(state: State, field_card_id: Int) { 61 - let query = 62 - pog.query("SELECT * FROM field_tiles WHERE card_id = $1") 63 - |> pog.parameter(pog.int(field_card_id)) 64 - |> pog.returning(field_tile.from_sql_row()) 65 - use field_rows <- rows.execute(query, state.db) 66 - use field <- rows.one(field_rows) 67 - use Nil <- result.try( 68 - output_action.FieldTile(field) 69 - |> output_message.OutputMessage(state.message_id) 70 - |> output_message.send(state.conn) 71 - |> result.map_error(rows.HandlerError), 72 - ) 73 - Ok(Nil) 74 - } 75 - 76 - fn on_notification(state: State, event: Event) { 77 - palabres.info("database fields event received") 78 - |> palabres.string("event", string.inspect(event)) 79 - |> palabres.log() 80 - 81 - let assert Ok(Nil) = case event { 82 - PlaceCard(field_card_id, _) -> push_field_card(state, field_card_id) 83 - UnplaceCard(field_card_id, _) -> 84 - output_action.FieldTileStub(field_card_id) 85 - |> output_message.OutputMessage(state.message_id) 86 - |> output_message.send(state.conn) 87 - |> result.map_error(rows.HandlerError) 88 - } 89 - notification_listener.continue(state) 90 - }
-85
server/src/handlers/listeners/fields_listener.gleam
··· 1 - import db/notification_listener 2 - import db/rows 3 - import dto/output_action 4 - import dto/output_message 5 - import gleam/dynamic/decode 6 - import gleam/result 7 - import gleam/string 8 - import mist 9 - import models/field 10 - import palabres 11 - import pog 12 - import websocket/state.{type State as WebsocketState} 13 - 14 - type State { 15 - State( 16 - account_id: String, 17 - message_id: String, 18 - conn: mist.WebsocketConnection, 19 - db: pog.Connection, 20 - ) 21 - } 22 - 23 - type Event { 24 - NewField(target: Int, subject: String) 25 - EditField(target: Int, subject: String) 26 - } 27 - 28 - fn event_decoder() { 29 - use event <- decode.field("event", decode.string) 30 - use target <- decode.field("target", decode.int) 31 - use subject <- decode.field("subject", decode.string) 32 - case event { 33 - "new_field" -> decode.success(NewField(target, subject)) 34 - "edit_field" -> decode.success(EditField(target, subject)) 35 - _ -> decode.failure(NewField(target, subject), "fields event") 36 - } 37 - } 38 - 39 - pub fn start( 40 - st: WebsocketState, 41 - conn: mist.WebsocketConnection, 42 - account_id: String, 43 - message_id: String, 44 - ) { 45 - notification_listener.new(State( 46 - account_id:, 47 - conn:, 48 - db: state.db_connection(st), 49 - message_id:, 50 - )) 51 - |> notification_listener.listen_to( 52 - state.notifications_connection(st), 53 - "fields:" <> account_id, 54 - ) 55 - |> notification_listener.on_notification(event_decoder(), on_notification) 56 - |> notification_listener.start() 57 - } 58 - 59 - fn push_field(state: State, field_id: Int) { 60 - let query = 61 - pog.query("SELECT * FROM fields WHERE id = $1") 62 - |> pog.parameter(pog.int(field_id)) 63 - |> pog.returning(field.from_sql_row()) 64 - use field_rows <- rows.execute(query, state.db) 65 - use field <- rows.one(field_rows) 66 - use Nil <- result.try( 67 - output_action.Field(field) 68 - |> output_message.OutputMessage(state.message_id) 69 - |> output_message.send(state.conn) 70 - |> result.map_error(rows.HandlerError), 71 - ) 72 - Ok(Nil) 73 - } 74 - 75 - fn on_notification(state: State, event: Event) { 76 - palabres.info("database fields event received") 77 - |> palabres.string("event", string.inspect(event)) 78 - |> palabres.log() 79 - 80 - let assert Ok(Nil) = case event { 81 - NewField(field_id, _) -> push_field(state, field_id) 82 - EditField(field_id, _) -> push_field(state, field_id) 83 - } 84 - notification_listener.continue(state) 85 - }
+5 -28
server/src/handlers/subscribe_handler.gleam
··· 1 - import db/notification_listener 2 1 import dto/channel 3 2 import gleam/result 4 - import gleam/string 5 - import handlers/listeners/card_accounts_listener 6 - import handlers/listeners/field_cards_listener 7 - import handlers/listeners/fields_listener 8 3 import mist 9 4 import websocket/state 10 5 11 6 pub fn handle( 12 7 st: state.State, 13 - conn: mist.WebsocketConnection, 8 + _conn: mist.WebsocketConnection, 14 9 message_id: String, 15 10 channel: channel.Channel, 16 11 ) -> Result(mist.Next(state.State, _msg), String) { 17 - use account_id <- state.account_id(st) 12 + use _account_id <- state.account_id(st) 13 + 18 14 use unsubscribe <- result.try(case channel { 19 - channel.Fields -> { 20 - use listener <- result.map( 21 - fields_listener.start(st, conn, account_id, message_id) 22 - |> result.map_error(string.inspect), 23 - ) 24 - fn() { notification_listener.unlisten(listener.data) } 25 - } 26 - channel.Deck -> { 27 - use listener <- result.map( 28 - card_accounts_listener.start(st, conn, account_id, message_id) 29 - |> result.map_error(string.inspect), 30 - ) 31 - fn() { notification_listener.unlisten(listener.data) } 32 - } 33 - channel.FieldTiles(field_id) -> { 34 - use listener <- result.map( 35 - field_cards_listener.start(st, conn, field_id, message_id) 36 - |> result.map_error(string.inspect), 37 - ) 38 - fn() { notification_listener.unlisten(listener.data) } 39 - } 15 + channel.Deck -> todo 40 16 }) 17 + 41 18 st 42 19 |> state.add_listener(message_id, unsubscribe) 43 20 |> mist.continue()
-6
server/src/models/account.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 4 3 pub type Account { 5 4 Account(id: String) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use id <- decode.field(0, decode.string) 10 - decode.success(Account(id:)) 11 5 } 12 6 13 7 pub fn to_json(account: Account) {
-7
server/src/models/card.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 4 3 pub type Card { 5 4 Card(id: Int, card_type_id: String) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use id <- decode.field(0, decode.int) 10 - use card_type_id <- decode.field(1, decode.string) 11 - decode.success(Card(id:, card_type_id:)) 12 5 } 13 6 14 7 pub fn to_json(citizen: Card) {
-6
server/src/models/card_account.gleam
··· 5 5 CardAccount(card_id: Int, account_id: String) 6 6 } 7 7 8 - pub fn from_sql_row() { 9 - use card_id <- decode.field(0, decode.int) 10 - use account_id <- decode.field(1, decode.string) 11 - decode.success(CardAccount(card_id:, account_id:)) 12 - } 13 - 14 8 pub fn to_json(field_card: CardAccount) { 15 9 json.object([ 16 10 #("card_id", json.int(field_card.card_id)),
-13
server/src/models/card_set.gleam
··· 8 8 CardSet(id: String, release_date: timestamp.Timestamp) 9 9 } 10 10 11 - pub fn from_sql_row() { 12 - use id <- decode.field(0, decode.string) 13 - use release_date <- decode.field(1, decode.string) 14 - case timestamp.parse_rfc3339(release_date) { 15 - Ok(release_date) -> decode.success(CardSet(id:, release_date:)) 16 - Error(error) -> 17 - decode.failure( 18 - CardSet(id:, release_date: timestamp.unix_epoch), 19 - string.inspect(error), 20 - ) 21 - } 22 - } 23 - 24 11 pub fn to_json(card_set: CardSet) { 25 12 json.object([ 26 13 #("id", json.string(card_set.id)),
-7
server/src/models/card_type.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 import models/card_class 4 3 5 4 pub type CardType { 6 5 CardType(id: String, class: card_class.CardClass) 7 - } 8 - 9 - pub fn from_sql_row() { 10 - use id <- decode.field(0, decode.string) 11 - use class <- decode.field(1, card_class.decoder()) 12 - decode.success(CardType(id:, class:)) 13 6 } 14 7 15 8 pub fn to_json(card_type: CardType) {
-9
server/src/models/citizen.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 import gleam/option 4 3 ··· 9 8 home_tile_id: option.Option(Int), 10 9 name: String, 11 10 ) 12 - } 13 - 14 - pub fn from_sql_row() { 15 - use id <- decode.field(0, decode.int) 16 - use species_id <- decode.field(1, decode.string) 17 - use name <- decode.field(2, decode.string) 18 - use home_tile_id <- decode.field(3, decode.optional(decode.int)) 19 - decode.success(Citizen(id:, species_id:, home_tile_id:, name:)) 20 11 } 21 12 22 13 pub fn to_json(citizen: Citizen) {
+9 -7
server/src/models/field.gleam
··· 1 + import db/sql 1 2 import gleam/dynamic/decode 2 3 import gleam/json 3 4 4 5 pub type Field { 5 6 Field(id: Int, name: String, account_id: String) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use id <- decode.field(0, decode.int) 10 - use name <- decode.field(1, decode.string) 11 - use account_id <- decode.field(2, decode.string) 12 - decode.success(Field(id:, name:, account_id:)) 13 7 } 14 8 15 9 pub fn to_json(field: Field) { ··· 26 20 use account_id <- decode.field("account_id", decode.string) 27 21 decode.success(Field(id:, name:, account_id:)) 28 22 } 23 + 24 + pub fn from_list_fields_for_account(row: sql.ListFieldsForAccountRow) { 25 + Field(id: row.id, name: row.name, account_id: row.account_id) 26 + } 27 + 28 + pub fn from_get_field_by_id(row: sql.GetFieldByIdRow) { 29 + Field(id: row.id, name: row.name, account_id: row.account_id) 30 + }
-9
server/src/models/field_tile.gleam
··· 11 11 ) 12 12 } 13 13 14 - pub fn from_sql_row() { 15 - use tile_id <- decode.field(0, decode.int) 16 - use account_id <- decode.field(1, decode.string) 17 - use field_id <- decode.field(2, decode.int) 18 - use grid_x <- decode.field(3, decode.int) 19 - use grid_y <- decode.field(4, decode.int) 20 - decode.success(FieldTile(tile_id:, account_id:, field_id:, grid_x:, grid_y:)) 21 - } 22 - 23 14 pub fn to_json(field_tile: FieldTile) { 24 15 json.object([ 25 16 #("tile_id", json.int(field_tile.tile_id)),
-6
server/src/models/resource.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 4 3 pub type Resource { 5 4 Resource(id: String) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use id <- decode.field(0, decode.string) 10 - decode.success(Resource(id:)) 11 5 } 12 6 13 7 pub fn to_json(resource: Resource) {
-6
server/src/models/species.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 4 3 pub type Species { 5 4 Species(id: String) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use id <- decode.field(0, decode.string) 10 - decode.success(Species(id:)) 11 5 } 12 6 13 7 pub fn to_json(species: Species) {
-8
server/src/models/species_need.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 4 3 pub type SpeciesNeed { 5 4 SpeciesNeed(species_id: String, resource_id: String, quantity: Int) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use species_id <- decode.field(0, decode.string) 10 - use resource_id <- decode.field(1, decode.string) 11 - use quantity <- decode.field(2, decode.int) 12 - decode.success(SpeciesNeed(species_id:, resource_id:, quantity:)) 13 5 } 14 6 15 7 pub fn to_json(need: SpeciesNeed) {
-8
server/src/models/tile.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 4 3 pub type Tile { 5 4 Tile(id: Int, tile_type_id: String, name: String) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use id <- decode.field(0, decode.int) 10 - use tile_type_id <- decode.field(1, decode.string) 11 - use name <- decode.field(2, decode.string) 12 - decode.success(Tile(id:, tile_type_id:, name:)) 13 5 } 14 6 15 7 pub fn to_json(tile: Tile) {
-9
server/src/models/tile_type.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 import models/tile_category 4 3 ··· 9 8 houses: Int, 10 9 employs: Int, 11 10 ) 12 - } 13 - 14 - pub fn from_sql_row() { 15 - use id <- decode.field(0, decode.string) 16 - use category <- decode.field(1, tile_category.decoder()) 17 - use houses <- decode.field(2, decode.int) 18 - use employs <- decode.field(3, decode.int) 19 - decode.success(TileType(id:, category:, houses:, employs:)) 20 11 } 21 12 22 13 pub fn to_json(card_type: TileType) {
-8
server/src/models/tile_type_consume.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 4 3 pub type TileTypeConsume { 5 4 TileTypeConsume(tile_type_id: String, resource_id: String, quantity: Int) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use tile_type_id <- decode.field(0, decode.string) 10 - use resource_id <- decode.field(1, decode.string) 11 - use quantity <- decode.field(2, decode.int) 12 - decode.success(TileTypeConsume(tile_type_id:, resource_id:, quantity:)) 13 5 } 14 6 15 7 pub fn to_json(consume: TileTypeConsume) {
-8
server/src/models/tile_type_produce.gleam
··· 1 - import gleam/dynamic/decode 2 1 import gleam/json 3 2 4 3 pub type TileTypeProduce { 5 4 TileTypeProduce(tile_type_id: String, resource_id: String, quantity: Int) 6 - } 7 - 8 - pub fn from_sql_row() { 9 - use tile_type_id <- decode.field(0, decode.string) 10 - use resource_id <- decode.field(1, decode.string) 11 - use quantity <- decode.field(2, decode.int) 12 - decode.success(TileTypeProduce(tile_type_id:, resource_id:, quantity:)) 13 5 } 14 6 15 7 pub fn to_json(produce: TileTypeProduce) {