From 80feadfbd25600a5b22cbb4467cfa517363dcb55 Mon Sep 17 00:00:00 2001 From: Juan Rodriguez Date: Mon, 13 May 2024 10:23:20 +0200 Subject: [PATCH] feat: delete link --- .env.development | 2 +- .gitignore | 7 +++--- app/controllers/link.cr | 52 +++++++++++++++++++++++++---------------- app/lib/controller.cr | 20 +++++++++++++++- app/lib/database.cr | 5 ++-- app/lib/errors.cr | 13 ++++++++--- app/models/link.cr | 1 + app/routes.cr | 6 ++++- bruno/Delete Link.bru | 11 +++++++++ bruno/Get Link.bru | 2 +- url-shortener.cr | 1 - 11 files changed, 86 insertions(+), 34 deletions(-) create mode 100644 bruno/Delete Link.bru diff --git a/.env.development b/.env.development index 22f3233..8bd0b90 100644 --- a/.env.development +++ b/.env.development @@ -1,2 +1,2 @@ -DATABASE_URL=sqlite3://./sqlite/data.db +DATABASE_URL=sqlite3://./sqlite/data.db?journal_mode=wal&synchronous=normal APP_URL=http://localhost:4000 diff --git a/.gitignore b/.gitignore index fe8d532..ba8d3ca 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /bin/ /.shards/ *.dwarf -*.db -.env.* -url-shortener +.DS_Store + +./sqlite/* +.env.production diff --git a/app/controllers/link.cr b/app/controllers/link.cr index eef1c27..26672ac 100644 --- a/app/controllers/link.cr +++ b/app/controllers/link.cr @@ -8,20 +8,16 @@ module App::Controllers::Link include App::Lib def call(env) - json_params = env.params.json.to_h - url = json_params.has_key?("url") ? json_params["url"] : nil - raise App::BadRequestException.new(env, {"url" => "Required field"}) if !url + body = parse_body(env, ["url"]) link = Link.new link.id = UUID.v4.to_s - link.url = url.to_s + link.url = body["url"].to_s link.slug = Random::Secure.urlsafe_base64(4) changeset = Database.insert(link) - if !changeset.valid? - errors = {"errors" => map_changeset_errors(changeset.errors)} - raise App::UnprocessableEntityException.new(env, errors) + raise App::UnprocessableEntityException.new(env, map_changeset_errors(changeset.errors)) end response = {"data" => App::Serializers::Link.new(link)} @@ -36,7 +32,8 @@ module App::Controllers::Link def call(env) slug = env.params.url["slug"] - link = Database.get_by!(Link, slug: slug) + link = Database.get_by(Link, slug: slug) + raise App::NotFoundException.new(env) if !link spawn do link.click_counter = link.click_counter! + 1 @@ -47,18 +44,19 @@ module App::Controllers::Link end end - env.redirect link.url! + env.redirect link.url!, 301 end end - class Get < App::Lib::BaseController + class Read < App::Lib::BaseController include App::Models include App::Lib def call(env) id = env.params.url["id"] - link = Database.get!(Link, id) + link = Database.get(Link, id) + raise App::NotFoundException.new(env) if !link response = {"data" => App::Serializers::Link.new(link)} response.to_json @@ -71,20 +69,17 @@ module App::Controllers::Link def call(env) id = env.params.url["id"] + body = parse_body(env, ["url"]) - json_params = env.params.json.to_h - url = json_params.has_key?("url") ? json_params["url"] : nil - raise App::BadRequestException.new(env, {"url" => "Required field"}) if !url + link = Database.get(Link, id) + raise App::NotFoundException.new(env) if !link - link = Database.get!(Link, id) - link.url = url.to_s + link.url = body["url"].to_s link.click_counter = 0 changeset = Database.update(link) - if !changeset.valid? - errors = {"errors" => map_changeset_errors(changeset.errors)} - raise App::UnprocessableEntityException.new(env, errors) + raise App::UnprocessableEntityException.new(env, map_changeset_errors(changeset.errors)) end response = {"data" => App::Serializers::Link.new(link)} @@ -92,5 +87,22 @@ module App::Controllers::Link end end - # TODO: delete + class Delete < App::Lib::BaseController + include App::Models + include App::Lib + + def call(env) + id = env.params.url["id"] + + link = Database.get(Link, id) + raise App::NotFoundException.new(env) if !link + + result = Database.raw_exec("DELETE FROM links WHERE id = (?)", link.id) # tempfix: Database.delete does not work + if result.rows_affected == 0 + raise App::UnprocessableEntityException.new(env, { "id" => ["Row delete failed"] }) + end + + env.response.status_code = 204 + end + end end diff --git a/app/lib/controller.cr b/app/lib/controller.cr index 9071ce5..0fd6808 100644 --- a/app/lib/controller.cr +++ b/app/lib/controller.cr @@ -1,11 +1,29 @@ module App::Lib abstract class BaseController - def map_changeset_errors(errors) + def map_changeset_errors(errors) errors.reduce({} of String => Array(String)) do |memo, error| memo[error[:field]] = memo[error[:field]]? || [] of String memo[error[:field]] << error[:message] memo end end + + def parse_body(env, fields) + json_params = env.params.json.to_h + missing_fields = [] of String + + fields.each do |field| + unless json_params.has_key?(field) + missing_fields << field + end + end + + unless missing_fields.empty? + error_message = missing_fields.map { |field| "#{field}: Required field" }.join(", ") + raise App::BadRequestException.new(env, error_message) + end + + json_params + end end end diff --git a/app/lib/database.cr b/app/lib/database.cr index c761652..5b6ab14 100644 --- a/app/lib/database.cr +++ b/app/lib/database.cr @@ -5,11 +5,10 @@ module App::Lib class Database extend Crecto::Repo - Query = Crecto::Repo::Query - Multi = Crecto::Repo::Multi - config do |conf| conf.uri = ENV["DATABASE_URL"] end + + Crecto::DbLogger.set_handler(STDOUT) end end diff --git a/app/lib/errors.cr b/app/lib/errors.cr index b53a538..46f2327 100644 --- a/app/lib/errors.cr +++ b/app/lib/errors.cr @@ -2,17 +2,24 @@ require "kemal" module App class BadRequestException < Kemal::Exceptions::CustomException - def initialize(context, message = Hash(String, String)) + def initialize(context, message : String) context.response.status_code = 400 context.response.print({ "error" => message }.to_json) super(context) end end + class NotFoundException < Kemal::Exceptions::CustomException + def initialize(context) + context.response.status_code = 404 + super(context) + end + end + class UnprocessableEntityException < Kemal::Exceptions::CustomException - def initialize(context, message = Hash(String, String)) + def initialize(context, message : Hash(String, Array(String))) context.response.status_code = 422 - context.response.print({ "error" => message }.to_json) + context.response.print({ "errors" => message }.to_json) super(context) end end diff --git a/app/models/link.cr b/app/models/link.cr index 0dd2a63..875a722 100644 --- a/app/models/link.cr +++ b/app/models/link.cr @@ -10,6 +10,7 @@ module App::Models field :click_counter, Int64, default: 0 end + unique_constraint :slug validate_required [:slug, :url] end end diff --git a/app/routes.cr b/app/routes.cr index 1e03abc..8bf4b82 100644 --- a/app/routes.cr +++ b/app/routes.cr @@ -14,7 +14,7 @@ module App end get "/api/links/:id" do |env| - Controllers::Link::Get.new.call(env) + Controllers::Link::Read.new.call(env) end post "/api/links" do |env| @@ -24,4 +24,8 @@ module App put "/api/links/:id" do |env| Controllers::Link::Update.new.call(env) end + + delete "/api/links/:id" do |env| + Controllers::Link::Delete.new.call(env) + end end diff --git a/bruno/Delete Link.bru b/bruno/Delete Link.bru new file mode 100644 index 0000000..59ae339 --- /dev/null +++ b/bruno/Delete Link.bru @@ -0,0 +1,11 @@ +meta { + name: Delete Link + type: http + seq: 7 +} + +delete { + url: {{baseUrl}}/api/links/ad9fb116-9e5b-45b7-b272-5285b579d2e4 + body: none + auth: none +} diff --git a/bruno/Get Link.bru b/bruno/Get Link.bru index 4611132..d0c4dca 100644 --- a/bruno/Get Link.bru +++ b/bruno/Get Link.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{baseUrl}}/api/links/0da9dc9d-c56c-4d4e-942e-1ebf05ed7090 + url: {{baseUrl}}/api/links/d017c966-c28b-4c4c-a83e-97bce81ee6e7 body: none auth: none } diff --git a/url-shortener.cr b/url-shortener.cr index 3da98d7..d1a7cb6 100644 --- a/url-shortener.cr +++ b/url-shortener.cr @@ -9,7 +9,6 @@ require "./app/routes" error 500 { |env| { "error" => "Internal Server Error" }.to_json } error 401 { |env| { "error" => "Unauthorized" }.to_json } -error 403 { |env| { "error" => "Forbidden" }.to_json } error 404 { |env| { "error" => "Not Found" }.to_json } Kemal.run