diff options
author | Mateus Cruz <mateuscolvr@gmail.com> | 2024-02-05 03:31:35 -0300 |
---|---|---|
committer | Mateus Cruz <mateuscolvr@gmail.com> | 2024-02-05 03:31:35 -0300 |
commit | 5a2f5bf9a12b270e15757df48afeb1c1db5b34b3 (patch) | |
tree | 2f1797364d78d935b4eb79269fd6b100fa77ec21 | |
parent | f0142aa1c3b4d7c2fd0b70c9e62c1ec033e445c2 (diff) |
add db queries
-rw-r--r-- | bin/dune | 10 | ||||
-rw-r--r-- | bin/main.ml | 14 | ||||
-rw-r--r-- | lib/client.ml | 4 | ||||
-rw-r--r-- | lib/dune | 4 | ||||
-rw-r--r-- | lib/operation.ml | 49 | ||||
-rw-r--r-- | lib/query.ml | 37 | ||||
-rw-r--r-- | lib/router.ml | 46 | ||||
-rw-r--r-- | lib/router.mli | 1 | ||||
-rw-r--r-- | lib/utils.ml | 121 | ||||
-rw-r--r-- | nginx.conf | 2 | ||||
-rw-r--r-- | script.sql | 12 |
11 files changed, 266 insertions, 34 deletions
@@ -5,17 +5,9 @@ (:standard -cclib -static -cclib -no-pie)) (libraries rinha - piaf routes eio_main - caqti-driver-postgresql logs.fmt fmt.tty - logs.threaded - caqti-eio - caqti - ppx_rapper_eio) - - (preprocess - (pps ppx_rapper))) + logs.threaded)) diff --git a/bin/main.ml b/bin/main.ml index 7e537a7..99cc0e0 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -4,9 +4,9 @@ open! StdLabels open Eio open Piaf -let request_handler Server.{ request; _ } = +let request_handler ~db_pool Server.{ request; _ } = match Rinha.Router.match_route request.meth request.target with - | Some handler -> handler request + | Some handler -> handler db_pool request | None -> Response.create `Not_found ;; @@ -17,10 +17,16 @@ let () = @@ fun sw -> let config = let interface = Net.Ipaddr.V4.any in - let port = 3000 in + let port = 8080 in `Tcp (interface, port) in let config = Server.Config.create config in - let server = Server.create ~config request_handler in + let db_uri = + Uri.make ~scheme:"postgres" ~userinfo:"admin:123" ~host:"db" ~path:"rinha" () + in + let db_pool = + Result.get_ok @@ Caqti_eio.connect_pool ~sw ~stdenv:(env :> Caqti_eio.stdenv) db_uri + in + let server = Server.create ~config (request_handler ~db_pool) in ignore @@ Server.Command.start ~sw env server ;; diff --git a/lib/client.ml b/lib/client.ml new file mode 100644 index 0000000..80a11b7 --- /dev/null +++ b/lib/client.ml @@ -0,0 +1,4 @@ +type t = + { id : int + ; mov_limit : int + } @@ -1,3 +1,5 @@ (library (name rinha) - (libraries piaf routes yojson)) + (libraries piaf routes yojson caqti caqti-driver-postgresql caqti-eio ppx_rapper_eio) + (preprocess + (pps ppx_rapper))) diff --git a/lib/operation.ml b/lib/operation.ml new file mode 100644 index 0000000..1fb1dd5 --- /dev/null +++ b/lib/operation.ml @@ -0,0 +1,49 @@ +module TransactionType = struct + type t = + | Credit + | Debit + + let t = + let encode = function + | Credit -> "credit" + | Debit -> "debit" + in + let decode = function + | "credit" -> Ok Credit + | "debit" -> Ok Debit + | _ -> Error "Invalid transaction type" + in + Caqti_type.(enum ~encode ~decode "transaction_type") + ;; +end + +type t = + | Transaction of + { transaction_type : TransactionType.t + ; value : int + ; description : string + } + | Balance of { client_id : int } + +type transaction = + { id : int + ; client_id : int + ; value : int + ; transaction_type : TransactionType.t + ; description : string + ; created_at : Ptime.t + } + +let decoder = + let open Utils.Decoder in + let open Syntax in + let transaction_type_decoder = + literal "c" *> return TransactionType.Credit + <|> literal "d" *> return TransactionType.Debit + in + (fun value transaction_type description -> + Transaction { value; description; transaction_type }) + <$> ("valor" <: int) + <*> ("tipo" <: transaction_type_decoder) + <*> ("descricao" <: string) +;; diff --git a/lib/query.ml b/lib/query.ml new file mode 100644 index 0000000..0fb6f7e --- /dev/null +++ b/lib/query.ml @@ -0,0 +1,37 @@ +type pool = ((module Rapper_helper.CONNECTION), Caqti_error.t) Caqti_eio.Pool.t + +let transaction_query = + [%rapper + execute + {sql| + INSERT INTO transactions (client_id, value, type, description) + VALUES (%int{client_id}, %int{value}, %Operation.TransactionType{transaction_type}, %string{description}) + |sql}] +;; + +let client_query = + let open Client in + [%rapper + get_opt + {sql| + SELECT @int{id}, @int{mov_limit} FROM clients WHERE id = %int{id} + |sql} + record_out] +;; + +let execute_operation ~client_id ~(op : Operation.t) pool = + match op with + | Transaction data -> + Caqti_eio.Pool.use + (fun conn -> + transaction_query + ~client_id + ~value:data.value + ~transaction_type:data.transaction_type + ~description:data.description + conn) + pool + | _ -> failwith "TODO" +;; + +let find_client id pool = Caqti_eio.Pool.use (fun conn -> client_query ~id conn) pool diff --git a/lib/router.ml b/lib/router.ml index cbff9fb..35cef34 100644 --- a/lib/router.ml +++ b/lib/router.ml @@ -14,16 +14,35 @@ module R = Map.Make (struct ;; end) -let transactions_route = - let handler client_id (request : Request.t) = - let json : Yojson.Safe.t = `Assoc [ "limite", `Int 100000; "saldo", `Int (-9098) ] in - Response.of_string ~body:(Yojson.Safe.to_string json) `OK - in - (s "clientes" / int / s "transacoes" /? nil) @--> handler -;; +module Handler = struct + let transaction client_id (db_pool : Query.pool) (request : Request.t) = + let client_opt = + Option.join @@ Result.to_option @@ Query.find_client client_id db_pool + in + match client_opt with + | Some client -> + let insert_result = + let body = Result.to_option @@ Body.to_string request.body in + let json = Option.map Yojson.Safe.from_string body in + let decoded_op = Option.bind json (Utils.Decoder.decode Operation.decoder) in + match decoded_op with + | Some op -> + (match Query.execute_operation ~client_id ~op db_pool with + | Ok _ as ok -> ok + | Error e -> Error (`DB e)) + | None -> Error (`Decoder "Invalid operation") + in + (match insert_result with + | Ok () -> + let json : Yojson.Safe.t = + `Assoc [ "limite", `Int 100000; "saldo", `Int (-9098) ] + in + Response.of_string ~body:(Yojson.Safe.to_string json) `OK + | Error _ -> Response.create (`Code 422)) + | None -> Response.create `Not_found + ;; -let balance_route = - let handler client_id (request : Request.t) = + let balance client_id (db_pool : Query.pool) (request : Request.t) = let json : Yojson.Safe.t = let balance = let total = `Int (-9098) in @@ -35,15 +54,16 @@ let balance_route = `Assoc [ "saldo", balance; "ultimas_transacoes", last_transactions ] in Response.of_string ~body:(Yojson.Safe.to_string json) `OK - in - (s "clientes" / int / s "extrato" /? nil) @--> handler -;; + ;; +end let routes = List.fold_left ~f:(fun acc (v, r) -> R.add_to_list v r acc) - [ `GET, balance_route; `POST, transactions_route; `POST, balance_route ] ~init:R.empty + [ `GET, (s "clientes" / int / s "extrato" /? nil) @--> Handler.balance + ; `POST, (s "clientes" / int / s "transacoes" /? nil) @--> Handler.transaction + ] ;; let router = R.map one_of routes diff --git a/lib/router.mli b/lib/router.mli deleted file mode 100644 index c0b5915..0000000 --- a/lib/router.mli +++ /dev/null @@ -1 +0,0 @@ -val match_route : Piaf.Method.t -> string -> (Piaf.Request.t -> Piaf.Response.t) option diff --git a/lib/utils.ml b/lib/utils.ml new file mode 100644 index 0000000..00e003a --- /dev/null +++ b/lib/utils.ml @@ -0,0 +1,121 @@ +module Decoder : sig + type 'a decoder + + val decode : 'a decoder -> Yojson.Safe.t -> 'a option + val return : 'a -> 'a decoder + val fail : 'a decoder + val bind : 'a decoder -> ('a -> 'b decoder) -> 'b decoder + val map : ('a -> 'b) -> 'a decoder -> 'b decoder + val string : string decoder + val literal : string -> string decoder + val int : int decoder + val field : string -> 'a decoder -> 'a decoder + val list : 'a decoder -> 'a list decoder + val one_of : 'a decoder list -> 'a decoder + + module Syntax : sig + val ( *> ) : 'a decoder -> 'b decoder -> 'b decoder + val ( <* ) : 'a decoder -> 'b decoder -> 'a decoder + val ( >>= ) : 'a decoder -> ('a -> 'b decoder) -> 'b decoder + val ( >>| ) : 'a decoder -> ('a -> 'b) -> 'b decoder + val ( <*> ) : ('a -> 'b) decoder -> 'a decoder -> 'b decoder + val ( <$> ) : ('a -> 'b) -> 'a decoder -> 'b decoder + val ( <: ) : string -> 'a decoder -> 'a decoder + val ( <|> ) : 'a decoder -> 'a decoder -> 'a decoder + end +end = struct + type 'a decoder = Yojson.Safe.t -> 'a option + + let decode decoder json = decoder json + let return v _ = Some v + let fail _ = None + + let map f decoder json = + match decoder json with + | Some d -> Some (f d) + | None -> None + ;; + + let bind decoder f json = + match decoder json with + | Some x -> f x `Null + | None -> None + ;; + + let rec one_of decoders json = + match decoders with + | [] -> None + | decoder :: tl -> + (match decoder json with + | Some _ as x -> x + | None -> one_of tl json) + ;; + + let string = function + | `String str -> Some str + | _ -> None + ;; + + let literal str = function + | `String s when String.equal s str -> Some s + | _ -> None + ;; + + let int = function + | `Int int -> Some int + | _ -> None + ;; + + let field key decoder json = + try decoder @@ Yojson.Safe.Util.member key json with + | _ -> None + ;; + + let list decoder json = + let rec helper acc = function + | [] -> Some [] + | hd :: tl -> + (match decoder hd with + | Some _ as x -> helper (x :: acc) tl + | None -> None) + in + match json with + | `List list -> Option.map List.rev (helper [] list) + | _ -> None + ;; + + module Syntax = struct + let ( *> ) decoder_a decoder_b yojson = + match decoder_a yojson with + | Some _ -> decoder_b yojson + | None -> None + ;; + + let ( <* ) decoder_a decoder_b yojson = + match decoder_a yojson with + | Some value -> Option.map (fun _ -> value) (decoder_b yojson) + | None -> None + ;; + + let ( >>= ) = bind + let ( >>| ) decoder f yojson = map f decoder yojson + + let ( <*> ) decoder_a decoder_b input = + match decoder_a input with + | Some f -> + (match decoder_b input with + | Some x -> Some (f x) + | None -> None) + | None -> None + ;; + + let ( <$> ) f decoder = decoder >>| f + let ( <: ) = field + + let ( <|> ) decoder_a decoder_b input = + match decoder_a input with + | Some _ as value -> value + | None -> decoder_b input + ;; + end +end @@ -12,7 +12,7 @@ http { } server { - listen 9999; # Lembra da porta 9999 obrigatória? + listen 9999; location / { proxy_pass http://api; @@ -1,14 +1,14 @@ CREATE TABLE clients ( id SERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, - limit INTEGER NOT NULL + mov_limit INTEGER NOT NULL ); -CREATE TYPE transaction_type AS ENUM ('c', 'd'); +CREATE TYPE transaction_type AS ENUM ('credit', 'debit'); CREATE TABLE transactions ( id SERIAL PRIMARY KEY, - client_id REFERENCES clients, + client_id INTEGER REFERENCES clients, value INTEGER NOT NULL, type transaction_type NOT NULL, description VARCHAR(10) NOT NULL, @@ -17,12 +17,13 @@ CREATE TABLE transactions ( CREATE TABLE balances ( id SERIAL PRIMARY KEY, - client_id REFERENCES clients, + client_id INTEGER REFERENCES clients, value INTEGER NOT NULL ); +DO $$ BEGIN - INSERT INTO clients (name, limit) + INSERT INTO clients (name, mov_limit) VALUES ('naruto', 1000 * 100), ('mob', 800 * 100), @@ -31,3 +32,4 @@ BEGIN INSERT INTO balances (client_id, value) SELECT id, 0 FROM clients; END; +$$ |