-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(groups): added group model and controller
- Loading branch information
1 parent
a02ea1c
commit 7d27aa1
Showing
13 changed files
with
396 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import app/config | ||
import app/db/models/group | ||
import app/serializers/base_serializer | ||
import app/serializers/group_serializer | ||
import gleam/http | ||
import gleam/int | ||
import gleam/list | ||
import gleam/result | ||
import wisp | ||
|
||
fn fetch_create_group_params( | ||
form_data: wisp.FormData, | ||
ctx: config.Context, | ||
next: fn(group.InsertableGroup) -> wisp.Response, | ||
) -> wisp.Response { | ||
let group = { | ||
use name <- result.try(list.key_find(form_data.values, "name")) | ||
|
||
Ok(group.InsertableGroup(name:, owner_id: config.get_user_or_crash(ctx).id)) | ||
} | ||
|
||
case group { | ||
Ok(params) -> next(params) | ||
Error(_) -> wisp.bad_request() | ||
} | ||
} | ||
|
||
fn handle_create_group(req: wisp.Request, ctx: config.Context) -> wisp.Response { | ||
use form <- wisp.require_form(req) | ||
use create_group_params <- fetch_create_group_params(form, ctx) | ||
|
||
case group.insert_group(create_group_params, ctx.db) { | ||
Ok(group) -> wisp.ok() |> wisp.json_body(group_serializer.run(group)) | ||
Error(_) -> | ||
wisp.internal_server_error() | ||
|> wisp.json_body(base_serializer.serialize_error( | ||
"Unable to create group!", | ||
)) | ||
} | ||
} | ||
|
||
fn handle_show_all_groups(ctx: config.Context) -> wisp.Response { | ||
case group.find_groups_of(config.get_user_or_crash(ctx).id, ctx.db) { | ||
Ok(groups) -> | ||
wisp.ok() |> wisp.json_body(group_serializer.run_array(groups)) | ||
|
||
Error(_) -> wisp.bad_request() | ||
} | ||
} | ||
|
||
fn fetch_group_id( | ||
group_id: String, | ||
handle: fn(Int) -> wisp.Response, | ||
) -> wisp.Response { | ||
case int.parse(group_id) { | ||
Ok(id) -> handle(id) | ||
Error(_) -> | ||
wisp.bad_request() | ||
|> wisp.json_body(base_serializer.serialize_error( | ||
"Invalid group id" <> group_id, | ||
)) | ||
} | ||
} | ||
|
||
fn handle_show_group(group_id: String, ctx: config.Context) -> wisp.Response { | ||
use id <- fetch_group_id(group_id) | ||
|
||
case group.find_by_id(id, ctx.db) { | ||
Ok(group) -> wisp.ok() |> wisp.json_body(group_serializer.run(group)) | ||
Error(_) -> wisp.not_found() | ||
} | ||
} | ||
|
||
pub fn controller(req: wisp.Request, ctx: config.Context) -> wisp.Response { | ||
case ctx.scoped_segments, req.method { | ||
[], http.Post -> handle_create_group(req, ctx) | ||
|
||
[], http.Get -> handle_show_all_groups(ctx) | ||
|
||
[group_id], http.Get -> handle_show_group(group_id, ctx) | ||
|
||
[group_id], http.Put -> { | ||
wisp.not_found() | ||
} | ||
|
||
[group_id], http.Delete -> wisp.method_not_allowed([http.Delete]) | ||
|
||
_, __ -> wisp.not_found() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
## Migrations cheat sheet | ||
```sql | ||
-- Original migration tracking | ||
CREATE TABLE migrations ( | ||
id INTEGER PRIMARY KEY, | ||
name TEXT, | ||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
); | ||
|
||
-- Basic table operations | ||
CREATE TABLE users ( | ||
id INTEGER PRIMARY KEY, | ||
name TEXT | ||
); | ||
|
||
ALTER TABLE users ADD COLUMN email TEXT; | ||
ALTER TABLE users RENAME TO old_users; | ||
DROP TABLE users; | ||
|
||
-- Indexes | ||
CREATE INDEX idx_user_email ON users(email); | ||
DROP INDEX idx_user_email; | ||
|
||
-- Foreign Keys with ON DELETE/UPDATE actions | ||
CREATE TABLE posts ( | ||
id INTEGER PRIMARY KEY, | ||
user_id INTEGER, | ||
FOREIGN KEY(user_id) REFERENCES users(id) | ||
ON DELETE CASCADE -- Delete posts when user deleted | ||
ON UPDATE CASCADE -- Update if user_id changes | ||
); | ||
|
||
CREATE TABLE comments ( | ||
id INTEGER PRIMARY KEY, | ||
post_id INTEGER, | ||
FOREIGN KEY(post_id) REFERENCES posts(id) | ||
ON DELETE SET NULL -- Set post_id to NULL when post deleted | ||
); | ||
|
||
CREATE TABLE logs ( | ||
id INTEGER PRIMARY KEY, | ||
user_id INTEGER, | ||
FOREIGN KEY(user_id) REFERENCES users(id) | ||
ON DELETE RESTRICT -- Prevent user deletion if logs exist | ||
); | ||
|
||
-- Triggers | ||
CREATE TRIGGER update_user_timestamp | ||
AFTER UPDATE ON users | ||
BEGIN | ||
UPDATE users SET updated_at = DATETIME('now') | ||
WHERE id = NEW.id; | ||
END; | ||
|
||
-- Views | ||
CREATE VIEW active_users AS | ||
SELECT * FROM users | ||
WHERE last_login > date('now', '-30 days'); | ||
|
||
-- Rolling back | ||
BEGIN TRANSACTION; | ||
-- your changes | ||
ROLLBACK; -- or COMMIT; | ||
|
||
-- Common column constraints | ||
NOT NULL | ||
UNIQUE | ||
DEFAULT value | ||
CHECK (condition) | ||
PRIMARY KEY | ||
AUTOINCREMENT | ||
|
||
-- Data types | ||
INTEGER | ||
TEXT | ||
REAL | ||
BLOB | ||
NULL | ||
DATETIME | ||
BOOLEAN | ||
|
||
-- Useful date functions | ||
DATE('now') | ||
DATETIME('now') | ||
DATE('now', '+1 day') | ||
DATE('now', '-1 month') | ||
STRFTIME('%Y-%m-%d', 'now') | ||
|
||
-- Version tracking example | ||
INSERT INTO migrations (name) VALUES ('create_users_20240101'); | ||
|
||
-- Common Queries with constraints | ||
PRAGMA foreign_keys = ON; -- Enable foreign key support | ||
PRAGMA journal_mode = WAL; -- Write-Ahead Logging mode | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
## How to add a model | ||
to use a DB resource in Okane, we need to add following utils to make it work | ||
|
||
- record itself | ||
- decoder | ||
- select query fields <- should be in the same order as the decoder fields | ||
- insert query record | ||
- insert query record encoder <- converts to cake params | ||
|
||
that's it :pray: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import app/db/connection | ||
import app/db/models/helpers | ||
import app/db/models/user | ||
import app/lib/generic_error | ||
import cake | ||
import cake/insert | ||
import cake/select | ||
import cake/where | ||
import decode/zero | ||
import gleam/dynamic | ||
import gleam/list | ||
import gleam/option.{type Option} | ||
import gleam/result | ||
import sqlight | ||
|
||
// 1. record | ||
pub type Group { | ||
Group( | ||
id: Int, | ||
name: String, | ||
owner_id: Int, | ||
created_at: String, | ||
deleted_at: Option(String), | ||
) | ||
} | ||
|
||
// 2. decoder | ||
|
||
fn decode_group(row: dynamic.Dynamic) { | ||
let decoder = { | ||
use id <- zero.field(0, zero.int) | ||
use name <- zero.field(1, zero.string) | ||
use owner_id <- zero.field(2, zero.int) | ||
use created_at <- zero.field(2, zero.string) | ||
use deleted_at <- zero.field(3, zero.optional(zero.string)) | ||
|
||
zero.success(Group(id:, name:, owner_id:, created_at:, deleted_at:)) | ||
} | ||
|
||
zero.run(row, decoder) | ||
} | ||
|
||
// 3. select props init | ||
fn select_group_query_init() { | ||
select.new() | ||
|> select.from_table("groups") | ||
|> select.selects([ | ||
select.col("groups.id"), | ||
select.col("griups.name"), | ||
select.col("groups.owner_id"), | ||
select.col("groups.created_at"), | ||
select.col("groups.deleted_at"), | ||
]) | ||
} | ||
|
||
// 4. insertable record | ||
pub type InsertableGroup { | ||
InsertableGroup(name: String, owner_id: Int) | ||
} | ||
|
||
// 5. insertable encoder | ||
fn insertable_group_encoder(group: InsertableGroup) { | ||
[group.name |> insert.string, group.owner_id |> insert.int] | ||
|> helpers.with_created_at_param | ||
|> insert.row | ||
} | ||
|
||
pub fn insert_group( | ||
insertable group: InsertableGroup, | ||
connection conn: sqlight.Connection, | ||
) { | ||
insert.from_records( | ||
records: [group], | ||
table_name: "groups", | ||
columns: ["name", "owner_id"] |> helpers.with_created_at_column, | ||
encoder: insertable_group_encoder, | ||
) | ||
|> insert.returning(["id", "name", "owner_id", "created_at", "deleted_at"]) | ||
|> insert.to_query | ||
|> cake.to_write_query | ||
|> connection.run_query_with(conn, decode_group) | ||
|> result.map_error(fn(e) { generic_error.new(e) }) | ||
|> helpers.first | ||
} | ||
|
||
pub fn find_by_id(id: Int, conn: sqlight.Connection) { | ||
select_group_query_init() | ||
|> select.where(where.col("groups.id") |> where.eq(where.int(id))) | ||
|> select.comment("find group by id") | ||
|> select.to_query | ||
|> cake.to_read_query | ||
|> connection.run_query_with(conn, decode_group) | ||
|> helpers.first | ||
} | ||
|
||
pub fn find_groups_of(user_id: Int, conn: sqlight.Connection) { | ||
select_group_query_init() | ||
|> select.where( | ||
// TODO: query where memberships has user_id = user_id | ||
where.or([where.col("groups.owner_id") |> where.eq(where.int(user_id))]), | ||
) | ||
|> select.to_query | ||
|> cake.to_read_query | ||
|> connection.run_query_with(conn, decode_group) | ||
|> generic_error.replace | ||
} |
Oops, something went wrong.