| 1 | // Simple TODO app using veb |
| 2 | // Run from this directory with `v run main.v` |
| 3 | // You can also enable vebs livereload feature with |
| 4 | // `v watch -d veb_livereload run main.v` |
| 5 | module main |
| 6 | |
| 7 | import veb |
| 8 | import db.sqlite |
| 9 | import os |
| 10 | import time |
| 11 | |
| 12 | struct Todo { |
| 13 | pub mut: |
| 14 | // `id` is the primary field. The attribute `sql: serial` acts like AUTO INCREMENT in sql. |
| 15 | // You can use this attribute if you want a unique id for each row. |
| 16 | id int @[primary; sql: serial] |
| 17 | name string |
| 18 | completed bool |
| 19 | created time.Time |
| 20 | updated time.Time |
| 21 | } |
| 22 | |
| 23 | pub struct Context { |
| 24 | veb.Context |
| 25 | pub mut: |
| 26 | // we can use this field to check whether we just created a TODO in our html templates |
| 27 | created_todo bool |
| 28 | } |
| 29 | |
| 30 | pub struct App { |
| 31 | veb.StaticHandler |
| 32 | pub: |
| 33 | // we can access the SQLITE database directly via `app.db` |
| 34 | db sqlite.DB |
| 35 | } |
| 36 | |
| 37 | // This method will only handle GET requests to the index page |
| 38 | @[get] |
| 39 | pub fn (app &App) index(mut ctx Context) veb.Result { |
| 40 | todos := sql app.db { |
| 41 | select from Todo |
| 42 | } or { return ctx.server_error('could not fetch todos from database!') } |
| 43 | return $veb.html() |
| 44 | } |
| 45 | |
| 46 | // This method will only handle POST requests to the index page |
| 47 | @['/'; post] |
| 48 | pub fn (app &App) create_todo(mut ctx Context, name string) veb.Result { |
| 49 | // We can receive form input fields as arguments in a route! |
| 50 | // we could also access the name field by doing `name := ctx.form['name']` |
| 51 | |
| 52 | // validate input field |
| 53 | if name == '' { |
| 54 | // set a form error |
| 55 | ctx.form_error = 'You must fill in all the fields!' |
| 56 | // send a HTTP 400 response code indicating that the form fields are incorrect |
| 57 | ctx.res.set_status(.bad_request) |
| 58 | // render the home page |
| 59 | return app.index(mut ctx) |
| 60 | } |
| 61 | |
| 62 | // create a new todo |
| 63 | todo := Todo{ |
| 64 | name: name |
| 65 | created: time.now() |
| 66 | updated: time.now() |
| 67 | } |
| 68 | |
| 69 | // insert the todo into our database |
| 70 | sql app.db { |
| 71 | insert todo into Todo |
| 72 | } or { return ctx.server_error('could not insert a new TODO in the database') } |
| 73 | |
| 74 | ctx.created_todo = true |
| 75 | |
| 76 | // render the home page |
| 77 | return app.index(mut ctx) |
| 78 | } |
| 79 | |
| 80 | @['/todo/:id/complete'; post] |
| 81 | pub fn (app &App) complete_todo(mut ctx Context, id int) veb.Result { |
| 82 | // first check if there exist a TODO record with `id` |
| 83 | todos := sql app.db { |
| 84 | select from Todo where id == id |
| 85 | } or { return ctx.server_error("could not fetch TODO's") } |
| 86 | if todos.len == 0 { |
| 87 | // return HTTP 404 when the TODO does not exist |
| 88 | ctx.res.set_status(.not_found) |
| 89 | return ctx.text('There is no TODO item with id=${id}') |
| 90 | } |
| 91 | |
| 92 | // update the TODO field |
| 93 | sql app.db { |
| 94 | update Todo set completed = true, updated = time.now() where id == id |
| 95 | } or { return ctx.server_error('could not update TODO') } |
| 96 | |
| 97 | // redirect client to the home page and tell the browser to sent a GET request |
| 98 | return ctx.redirect('/', typ: .see_other) |
| 99 | } |
| 100 | |
| 101 | @['/todo/:id/delete'; post] |
| 102 | pub fn (app &App) delete_todo(mut ctx Context, id int) veb.Result { |
| 103 | // first check if there exist a TODO record with `id` |
| 104 | todos := sql app.db { |
| 105 | select from Todo where id == id |
| 106 | } or { return ctx.server_error("could not fetch TODO's") } |
| 107 | if todos.len == 0 { |
| 108 | // return HTTP 404 when the TODO does not exist |
| 109 | ctx.res.set_status(.not_found) |
| 110 | return ctx.text('There is no TODO item with id=${id}') |
| 111 | } |
| 112 | |
| 113 | // prevent hackers from deleting TODO's that are not completed ;) |
| 114 | to_be_deleted := todos[0] |
| 115 | if !to_be_deleted.completed { |
| 116 | return ctx.request_error('You must first complete a TODO before you can delete it!') |
| 117 | } |
| 118 | |
| 119 | // delete the todo |
| 120 | sql app.db { |
| 121 | delete from Todo where id == id |
| 122 | } or { return ctx.server_error('could not delete TODO') } |
| 123 | |
| 124 | // redirect client to the home page and tell the browser to sent a GET request |
| 125 | return ctx.redirect('/', typ: .see_other) |
| 126 | } |
| 127 | |
| 128 | fn main() { |
| 129 | os.chdir(os.dir(@FILE))! |
| 130 | // create a new App instance with a connection to the database |
| 131 | mut app := &App{ |
| 132 | db: sqlite.connect('todo.db')! |
| 133 | } |
| 134 | |
| 135 | // mount the assets folder at `/assets/` |
| 136 | app.handle_static('assets', false)! |
| 137 | |
| 138 | // create the table in our database, if it doesn't exist |
| 139 | sql app.db { |
| 140 | create table Todo |
| 141 | }! |
| 142 | |
| 143 | // start our app at port 8080 |
| 144 | veb.run[App, Context](mut app, 8080) |
| 145 | } |
| 146 | |