Compare commits

...

22 Commits

Author SHA1 Message Date
503016d5b3 fix: docs 2025-12-24 23:46:10 -03:00
cbc2646c3f chore: add swagger to readme 2025-12-24 23:44:36 -03:00
027101081b chore: add readme and AI instructions 2025-12-24 23:41:54 -03:00
a76999fc71 chore: add gitignore 2025-12-24 23:41:25 -03:00
30398245e2 fix: add to proxy network 2025-12-24 18:38:49 -03:00
74c1e6721e refactor: expose through nginx instead 2025-12-24 17:46:36 -03:00
bacc3f533a refactor: port binding 2025-12-24 17:29:19 -03:00
65838bac74 docs: update swagger 2025-12-24 17:14:22 -03:00
01dc6bb462 fix: return always data property in response 2025-12-24 17:07:10 -03:00
a82adb90c2 build: add docker setup 2025-12-24 17:05:14 -03:00
6f419ece3d feat: add cors middleware 2025-12-24 16:33:13 -03:00
56209011ec fix: updatecontact table to map model fields exactly 2025-12-12 22:07:49 -03:00
99d035c915 refactor: enhance logic to handle missing db or table scenarios 2025-11-03 00:20:19 -03:00
01a7f33054 refactor: add field to APIError struct 2025-11-02 23:15:45 -03:00
92f13fba22 feat: add docs 2025-11-02 23:15:45 -03:00
3b1b05d5a6 feat: impl requests handllers for contacts and healthcheck 2025-11-02 23:15:45 -03:00
4a28259dc8 refactor: update interface contract and applies some fixes 2025-11-02 23:15:45 -03:00
af511203a4 refactor: updates delete method to return affected rows count 2025-11-02 23:15:45 -03:00
e06c30635e feat: add contact repository and base repository for shared db logic 2025-11-02 23:15:45 -03:00
ffdaa22aa3 feat: add contact model 2025-11-02 23:15:45 -03:00
1f4711edb6 feat: add db config and setup 2025-11-02 23:15:45 -03:00
de58856289 build: add deps 2025-11-02 23:14:20 -03:00
23 changed files with 1995 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
set_dev_env.sh

11
AI.md Normal file
View File

@@ -0,0 +1,11 @@
# AI Training Policy
This codebase is NOT intended for AI/ML training.
Do not use this code for:
- AI model training
- Code generation datasets
- Machine learning purposes
- Automated code suggestion systems
Permission is not granted for AI ingestion or training.

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM golang:1.25.3-alpine AS builder
WORKDIR /app
COPY . .
# Build
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -a -o main ./cmd/server
###########
FROM alpine:3.22.2
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copy the pre-built binary
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]

27
README.md Normal file
View File

@@ -0,0 +1,27 @@
## 🚫 AI/ML Training Restrictions
**This code is NOT suitable for AI training purposes.**
The code quality may contain security issues or anti-patterns.
Do not include this repository in any AI/ML training datasets.
# AgendaWeb
🇪🇸
Este sitio web es un remake de una de las lecciones en desarrollo web del curso "Desarrollo Web Completo con HTML5, CSS3, JS AJAX PHP y MySQL" por Juan Pablo De La Torre Valdez, con el objetivo de emplear intensivamente las herramientas nuevas y no tan nuevas que ofrece Angular. Adicionalmente el lado del backend esta desarrollado en Go con la finalidad de desarrollar competencias y expertise con este lenguaje.
API docs con [swagger](https://agenda-web-go.gabilandia.com/swagger)
Puedes ver el sitio original [aquí](https://agendaproject.epizy.com)
Y el repositorio [acá](https://github.com/gabdlr/projects-AgendaWeb)
El código del frontend esta en este [repositorio](https://gitea.gabilandia.com/gabdlr/agenda-web)
Que lo disfrutes!
🇺🇸
This web site is a remake of on the lessons from a web development course "Desarrollo Web Completo con HTML5, CSS3, JS AJAX PHP y MySQL" by Juan Pablo De La Torre Valdez, with the intention of intensively using newer and not so new tools offered by Angular. Aditionally the backend side has been developed using Go with the goal of developing proficiency and expetise with this language.
API docs with [swagger](https://agenda-web-go.gabilandia.com/swagger)
You can take a look a the original site [here](https://agendaproject.epizy.com)
And it's repository [here](https://github.com/gabdlr/projects-AgendaWeb)
The frontend code is in this [repository](https://gitea.gabilandia.com/gabdlr/agenda-web)
Enjoy!

41
cmd/server/main.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"log"
"net/http"
_ "gitea.gabilandia.com/gabdlr/agenda-web-go/docs"
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/database"
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/handler"
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/middleware"
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/repository"
httpSwagger "github.com/swaggo/http-swagger"
)
// @title Contacts API
// @version 1.0
// @description A simple Contacts CRUD API
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email gabriel.delosrios@tutamail.com
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
// @host agenda-web-go.gabilandia.com
// @BasePath /
func main() {
err := database.InitDB()
defer database.CloseDB()
if err != nil {
log.Fatal("Database connection failed:", err)
}
mux := http.NewServeMux()
contactRepo := repository.NewContactRepository(database.DB)
handler.HandleContacts(mux, contactRepo)
handler.HandlHealthChecks(mux)
mux.HandleFunc("/swagger/", httpSwagger.WrapHandler)
log.Fatal(http.ListenAndServe(":8080", middleware.CorsMiddleware(mux)))
}

23
docker-compose.yml Normal file
View File

@@ -0,0 +1,23 @@
services:
go-app:
build: .
container_name: agenda-web-go
environment:
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- ALLOWED_ORIGIN=${ALLOWED_ORIGIN}
networks:
- db-network
- proxy-network
volumes:
- ./:/app
restart: unless-stopped
networks:
db-network:
external: true
proxy-network:
external: true

380
docs/docs.go Normal file
View File

@@ -0,0 +1,380 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"email": "gabriel.delosrios@tutamail.com"
},
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/contacts": {
"get": {
"description": "Get a list of all contacts",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Get all contacts",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Contact"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
},
"post": {
"description": "Create a new contact with the provided data",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Create a new contact",
"parameters": [
{
"description": "Contact object",
"name": "contact",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Contact"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Contact"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
}
},
"/contacts/{id}": {
"get": {
"description": "Get a single contact by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Get a contact by ID",
"parameters": [
{
"type": "integer",
"description": "Contact ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Contact"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
},
"put": {
"description": "Update an existing contact by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Update a contact",
"parameters": [
{
"type": "integer",
"description": "Contact ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Contact object",
"name": "contact",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Contact"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
},
"delete": {
"description": "Delete a contact by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Delete a contact",
"parameters": [
{
"type": "integer",
"description": "Contact ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
}
}
},
"definitions": {
"handler.APIError": {
"type": "object",
"properties": {
"code": {
"description": "Error code",
"type": "integer"
},
"details": {
"description": "Additional error details",
"type": "string"
},
"field": {
"description": "Field name if applicable",
"type": "string",
"example": "name"
},
"message": {
"description": "Human-readable error message",
"type": "string"
}
}
},
"handler.APIResponse": {
"type": "object",
"properties": {
"data": {
"description": "The response data"
},
"errors": {
"description": "List of errors if any occurred",
"type": "array",
"items": {
"$ref": "#/definitions/handler.APIError"
}
},
"message": {
"description": "Optional message",
"type": "string"
},
"success": {
"description": "Indicates if the request was successful",
"type": "boolean"
}
}
},
"models.Contact": {
"type": "object",
"properties": {
"company": {
"description": "Company the contact works for",
"type": "string"
},
"id": {
"description": "ID is the unique identifier for the contact",
"type": "integer"
},
"name": {
"description": "Name of the contact",
"type": "string"
},
"phone": {
"description": "Phone number in international format",
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "agenda-web-go.gabilandia.com",
BasePath: "/",
Schemes: []string{},
Title: "Contacts API",
Description: "A simple Contacts CRUD API",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

356
docs/swagger.json Normal file
View File

@@ -0,0 +1,356 @@
{
"swagger": "2.0",
"info": {
"description": "A simple Contacts CRUD API",
"title": "Contacts API",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"email": "gabriel.delosrios@tutamail.com"
},
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
},
"version": "1.0"
},
"host": "agenda-web-go.gabilandia.com",
"basePath": "/",
"paths": {
"/contacts": {
"get": {
"description": "Get a list of all contacts",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Get all contacts",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Contact"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
},
"post": {
"description": "Create a new contact with the provided data",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Create a new contact",
"parameters": [
{
"description": "Contact object",
"name": "contact",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Contact"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Contact"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
}
},
"/contacts/{id}": {
"get": {
"description": "Get a single contact by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Get a contact by ID",
"parameters": [
{
"type": "integer",
"description": "Contact ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Contact"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
},
"put": {
"description": "Update an existing contact by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Update a contact",
"parameters": [
{
"type": "integer",
"description": "Contact ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Contact object",
"name": "contact",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Contact"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
},
"delete": {
"description": "Delete a contact by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Delete a contact",
"parameters": [
{
"type": "integer",
"description": "Contact ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.APIResponse"
}
}
}
}
}
},
"definitions": {
"handler.APIError": {
"type": "object",
"properties": {
"code": {
"description": "Error code",
"type": "integer"
},
"details": {
"description": "Additional error details",
"type": "string"
},
"field": {
"description": "Field name if applicable",
"type": "string",
"example": "name"
},
"message": {
"description": "Human-readable error message",
"type": "string"
}
}
},
"handler.APIResponse": {
"type": "object",
"properties": {
"data": {
"description": "The response data"
},
"errors": {
"description": "List of errors if any occurred",
"type": "array",
"items": {
"$ref": "#/definitions/handler.APIError"
}
},
"message": {
"description": "Optional message",
"type": "string"
},
"success": {
"description": "Indicates if the request was successful",
"type": "boolean"
}
}
},
"models.Contact": {
"type": "object",
"properties": {
"company": {
"description": "Company the contact works for",
"type": "string"
},
"id": {
"description": "ID is the unique identifier for the contact",
"type": "integer"
},
"name": {
"description": "Name of the contact",
"type": "string"
},
"phone": {
"description": "Phone number in international format",
"type": "string"
}
}
}
}
}

231
docs/swagger.yaml Normal file
View File

@@ -0,0 +1,231 @@
basePath: /
definitions:
handler.APIError:
properties:
code:
description: Error code
type: integer
details:
description: Additional error details
type: string
field:
description: Field name if applicable
example: name
type: string
message:
description: Human-readable error message
type: string
type: object
handler.APIResponse:
properties:
data:
description: The response data
errors:
description: List of errors if any occurred
items:
$ref: '#/definitions/handler.APIError'
type: array
message:
description: Optional message
type: string
success:
description: Indicates if the request was successful
type: boolean
type: object
models.Contact:
properties:
company:
description: Company the contact works for
type: string
id:
description: ID is the unique identifier for the contact
type: integer
name:
description: Name of the contact
type: string
phone:
description: Phone number in international format
type: string
type: object
host: agenda-web-go.gabilandia.com
info:
contact:
email: gabriel.delosrios@tutamail.com
name: API Support
description: A simple Contacts CRUD API
license:
name: MIT
url: https://opensource.org/licenses/MIT
termsOfService: http://swagger.io/terms/
title: Contacts API
version: "1.0"
paths:
/contacts:
get:
consumes:
- application/json
description: Get a list of all contacts
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.APIResponse'
- properties:
data:
items:
$ref: '#/definitions/models.Contact'
type: array
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.APIResponse'
summary: Get all contacts
tags:
- contacts
post:
consumes:
- application/json
description: Create a new contact with the provided data
parameters:
- description: Contact object
in: body
name: contact
required: true
schema:
$ref: '#/definitions/models.Contact'
produces:
- application/json
responses:
"201":
description: Created
schema:
allOf:
- $ref: '#/definitions/handler.APIResponse'
- properties:
data:
$ref: '#/definitions/models.Contact'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.APIResponse'
summary: Create a new contact
tags:
- contacts
/contacts/{id}:
delete:
consumes:
- application/json
description: Delete a contact by ID
parameters:
- description: Contact ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handler.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.APIResponse'
summary: Delete a contact
tags:
- contacts
get:
consumes:
- application/json
description: Get a single contact by its ID
parameters:
- description: Contact ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.APIResponse'
- properties:
data:
$ref: '#/definitions/models.Contact'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handler.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.APIResponse'
summary: Get a contact by ID
tags:
- contacts
put:
consumes:
- application/json
description: Update an existing contact by ID
parameters:
- description: Contact ID
in: path
name: id
required: true
type: integer
- description: Contact object
in: body
name: contact
required: true
schema:
$ref: '#/definitions/models.Contact'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handler.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.APIResponse'
summary: Update a contact
tags:
- contacts
swagger: "2.0"

29
go.mod
View File

@@ -1,3 +1,32 @@
module gitea.gabilandia.com/gabdlr/agenda-web-go module gitea.gabilandia.com/gabdlr/agenda-web-go
go 1.25.1 go 1.25.1
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-openapi/swag v0.25.1 // indirect
github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
github.com/go-openapi/swag/loading v0.25.1 // indirect
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/swaggo/http-swagger v1.3.4 // indirect
github.com/swaggo/swag v1.16.6 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

83
go.sum Normal file
View File

@@ -0,0 +1,83 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8=
github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@@ -0,0 +1,125 @@
package database
import (
"database/sql"
"fmt"
"log"
"os"
"regexp"
"time"
_ "github.com/go-sql-driver/mysql"
)
var DB *sql.DB
func InitDB() error {
if err := ensureDatabaseExists(); err != nil {
return err
}
if err := connectToDatabase(); err != nil {
return err
}
if err := ensureTablesExist(); err != nil {
return err
}
log.Println("Database initialized successfully")
return nil
}
func ensureDatabaseExists() error {
dsn := getDSN("")
db, err := sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("failed to connect to database server: %w", err)
}
defer db.Close()
// Get database name from environment
dbName := os.Getenv("DB_NAME")
// Check if database exists, create if not
var exists bool
err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = ?)", dbName).Scan(&exists)
if err != nil {
return fmt.Errorf("failed to check database existence: %w", err)
}
if !exists {
if !isValidDatabaseName(dbName) {
return fmt.Errorf("invalid database name: %s", dbName)
}
_, err = db.Exec("CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
if err != nil {
return fmt.Errorf("failed to create database: %w", err)
}
}
return nil
}
func connectToDatabase() error {
dsn := getDSN(os.Getenv("DB_NAME"))
var err error
DB, err = sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
DB.SetMaxOpenConns(25)
DB.SetMaxIdleConns(25)
DB.SetConnMaxLifetime(5 * time.Minute)
return nil
}
func ensureTablesExist() error {
_, err := DB.Exec(`
CREATE TABLE IF NOT EXISTS contacts (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
company VARCHAR(120) NOT NULL,
phone VARCHAR(15) NOT NULL
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`)
if err != nil {
return fmt.Errorf("failed to create contacts table: %w", err)
}
return nil
}
func CloseDB() {
if DB != nil {
DB.Close()
}
}
func getDSN(dbName string) string {
user := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
host := os.Getenv("DB_HOST")
port := os.Getenv("DB_PORT")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/", user, password, host, port)
if dbName != "" {
dsn += dbName
}
dsn += "?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci"
return dsn
}
func isValidDatabaseName(name string) bool {
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, name)
return matched && len(name) > 0 && len(name) <= 64
}

View File

@@ -0,0 +1,47 @@
package handler
import (
"net/http"
"sync"
)
type Route struct {
pattern string
handler func(w http.ResponseWriter, r *http.Request)
}
type baseHandler struct {
mux *http.ServeMux
registeredRoutes map[string]bool
Routes []Route
}
var (
bh *baseHandler
bhOnce sync.Once
bhMu sync.RWMutex
)
func NewBaseHandler(mux *http.ServeMux, routes []Route) *baseHandler {
bhOnce.Do(func() {
bh = &baseHandler{
mux: mux,
registeredRoutes: make(map[string]bool),
Routes: routes,
}
})
bhMu.Lock()
defer bhMu.Unlock()
bh.registerRoutes(routes)
return bh
}
func (b *baseHandler) registerRoutes(routes []Route) {
for _, route := range routes {
if !bh.registeredRoutes[route.pattern] {
b.mux.HandleFunc(route.pattern, route.handler)
bh.registeredRoutes[route.pattern] = true
}
}
}

View File

@@ -0,0 +1,198 @@
package handler
import (
"encoding/json"
"net/http"
"strconv"
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/models"
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/repository"
)
type ContactHandler struct {
repository repository.Repository[models.Contact]
}
const required_field = "is required"
const invalid_id = "Invalid ID"
func HandleContacts(mux *http.ServeMux, repo repository.Repository[models.Contact]) {
ch := &ContactHandler{repository: repo}
routes := []Route{
{"GET /contacts", ch.getAll},
{"GET /contacts/{id}", ch.getByID},
{"POST /contacts", ch.create},
{"PUT /contacts/{id}", ch.update},
{"DELETE /contacts/{id}", ch.delete},
}
NewBaseHandler(mux, routes)
}
// GetAll godoc
// @Summary Get all contacts
// @Description Get a list of all contacts
// @Tags contacts
// @Accept json
// @Produce json
// @Success 200 {object} APIResponse{data=[]models.Contact}
// @Failure 500 {object} APIResponse
// @Router /contacts [get]
func (h *ContactHandler) getAll(w http.ResponseWriter, r *http.Request) {
contacts, err := h.repository.GetAll()
if err != nil {
InternalError(w, err)
return
}
JSONSuccess(w, contacts, "Contact list retrieved successfully", http.StatusOK)
}
// GetByID godoc
// @Summary Get a contact by ID
// @Description Get a single contact by its ID
// @Tags contacts
// @Accept json
// @Produce json
// @Param id path int true "Contact ID"
// @Success 200 {object} APIResponse{data=models.Contact}
// @Failure 400 {object} APIResponse
// @Failure 404 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Router /contacts/{id} [get]
func (h *ContactHandler) getByID(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
BadRequest(w, ErrInvalidFormat, invalid_id)
return
}
contact, err := h.repository.GetByID(id)
if err != nil {
InternalError(w, err)
return
}
if contact == nil {
NotFound(w, ErrNotFound, "Contact not found")
return
}
JSONSuccess(w, contact, "Contact retrieved successfully", http.StatusOK)
}
// Create godoc
// @Summary Create a new contact
// @Description Create a new contact with the provided data
// @Tags contacts
// @Accept json
// @Produce json
// @Param contact body models.Contact true "Contact object"
// @Success 201 {object} APIResponse{data=models.Contact}
// @Failure 400 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Router /contacts [post]
func (h *ContactHandler) create(w http.ResponseWriter, r *http.Request) {
var contact models.Contact
if err := json.NewDecoder(r.Body).Decode(&contact); err != nil {
BadRequest(w, ErrInvalidJSON, "Error parsing the contact")
return
}
if contact.Name == "" {
err := ErrMissingRequired
err.Field = "name"
BadRequest(w, err, RequiredFieldErr("name"))
return
}
if contact.Company == "" {
err := ErrMissingRequired
err.Field = "company"
BadRequest(w, err, RequiredFieldErr("company"))
return
}
if contact.Phone == "" {
err := ErrMissingRequired
err.Field = "phone"
BadRequest(w, err, RequiredFieldErr("phone"))
return
}
id, err := h.repository.Create(&contact)
if err != nil {
InternalError(w, err)
return
}
contact.ID = int(id)
JSONSuccess(w, contact, "Contact created successfully", http.StatusCreated)
}
// Update godoc
// @Summary Update a contact
// @Description Update an existing contact by ID
// @Tags contacts
// @Accept json
// @Produce json
// @Param id path int true "Contact ID"
// @Param contact body models.Contact true "Contact object"
// @Success 200 {object} APIResponse
// @Failure 400 {object} APIResponse
// @Failure 404 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Router /contacts/{id} [put]
func (h *ContactHandler) update(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
BadRequest(w, ErrInvalidFormat, invalid_id)
return
}
var contact models.Contact
if err := json.NewDecoder(r.Body).Decode(&contact); err != nil {
BadRequest(w, ErrInvalidJSON, "Error parsing the contact")
return
}
contact.ID = id
updateErr := h.repository.Update(&contact)
if updateErr != nil {
InternalError(w, updateErr)
return
}
JSONSuccess(w, nil, "Contact updated successfully", http.StatusOK)
}
// Delete godoc
// @Summary Delete a contact
// @Description Delete a contact by ID
// @Tags contacts
// @Accept json
// @Produce json
// @Param id path int true "Contact ID"
// @Success 200 {object} APIResponse
// @Failure 400 {object} APIResponse
// @Failure 404 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Router /contacts/{id} [delete]
func (h *ContactHandler) delete(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
BadRequest(w, ErrInvalidFormat, invalid_id)
return
}
rowsAffected, err := h.repository.Delete(id)
if err != nil {
InternalError(w, err)
return
}
if rowsAffected == 0 {
BadRequest(w, ErrNotFound, "Contact not found")
return
}
JSONSuccess(w, nil, "Contact deleted successfully", http.StatusOK)
}

View File

@@ -0,0 +1,88 @@
package handler
import (
"fmt"
"net/http"
)
/*
Domain (First Digit)
1xxx - Request/Input Errors
2xxx - Authentication/Authorization
3xxx - Business Logic/Validation
4xxx - Data/Resource Errors
5xxx - System/External Service Errors
Category (Second Digit)
x0xx - General/Generic errors in that domain
x1xx - Format/Structure errors
x2xx - Validation/Constraint errors
x3xx - State/Workflow errors
x4xx - Permission/Access errors
Specific Error (Last Two Digits)
xx00-xx99 - Specific error instances
*/
// Domains
const (
DomainRequest = 1000
DomainAuth = 2000
DomainData = 4000
DomainSystem = 5000
)
const (
CategoryGeneral = 0
CategoryFormat = 100
CategoryValidation = 200
CategoryState = 300
CategoryPermission = 400
)
// Err code Domain + Category + Specific
const (
CodeInvalidJSON = DomainRequest + CategoryFormat + 1
InvalidParamFormat = DomainRequest + CategoryFormat + 0
CodeValidationFailed = DomainRequest + CategoryValidation + 0
CodeMissingRequired = DomainRequest + CategoryValidation + 1
CodeNotFound = DomainData + CategoryPermission + 0
CodeInternalError = DomainSystem + CategoryGeneral + 0
)
var (
ErrInvalidJSON = APIError{Code: CodeInvalidJSON, Message: "Invalid JSON in request body"}
ErrValidation = APIError{Code: CodeValidationFailed, Message: "Validation failed"}
ErrMissingRequired = APIError{Code: CodeMissingRequired, Message: "Required field is missing"}
ErrNotFound = APIError{Code: CodeNotFound, Message: "Resource not found"}
ErrInternalServer = APIError{Code: CodeInternalError, Message: "Internal server error"}
ErrInvalidFormat = APIError{Code: InvalidParamFormat, Message: "The given param doesn't match the format expectations"}
)
// Helper functions for common errors
func BadRequest(w http.ResponseWriter, err APIError, details string) {
JSONError(w, err.Message, details, err.Code, http.StatusBadRequest)
}
func NotFound(w http.ResponseWriter, err APIError, details string) {
JSONError(w, err.Message, details, err.Code, http.StatusNotFound)
}
func InternalError(w http.ResponseWriter, err error) {
JSONError(w, ErrInternalServer.Message, err.Error(), ErrInternalServer.Code, http.StatusInternalServerError)
}
func RequiredFieldErr(field string) string {
return (fmt.Sprintf("%s %s", field, required_field))
}

View File

@@ -0,0 +1,16 @@
package handler
import (
"encoding/json"
"net/http"
)
func HandlHealthChecks(mux *http.ServeMux) {
routes := []Route{{"GET /health", healthCheck}}
NewBaseHandler(mux, routes)
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

View File

@@ -0,0 +1,65 @@
package handler
import (
"encoding/json"
"net/http"
)
type APIResponse struct {
// Indicates if the request was successful
Success bool `json:"success"`
// The response data
Data any `json:"data"`
// List of errors if any occurred
Errors []APIError `json:"errors,omitempty"`
// Optional message
Message string `json:"message,omitempty"`
}
type APIError struct {
// Error code
Code int `json:"code"`
// Human-readable error message
Message string `json:"message"`
// Additional error details
Details string `json:"details,omitempty"`
// Field name if applicable
Field string `json:"field,omitempty" example:"name"`
}
const content_type = "Content-Type"
const application_json = "application/json"
func JSONSuccess(w http.ResponseWriter, data any, message string, statusCode int) {
w.Header().Set(content_type, application_json)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Data: data,
Message: message,
})
}
func JSONError(w http.ResponseWriter, message, details string, code, statusCode int) {
w.Header().Set(content_type, application_json)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(APIResponse{
Success: false,
Errors: []APIError{
{
Code: code,
Message: message,
Details: details,
},
},
})
}
func JSONErrors(w http.ResponseWriter, errors []APIError, statusCode int) {
w.Header().Set(content_type, application_json)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(APIResponse{
Success: false,
Errors: errors,
})
}

View File

@@ -0,0 +1,19 @@
package middleware
import (
"net/http"
"os"
)
func CorsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", os.Getenv("ALLOWED_ORIGIN"))
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,13 @@
package models
// Contact represents a contact entity
type Contact struct {
// ID is the unique identifier for the contact
ID int `json:"id" db:"id"`
// Name of the contact
Name string `json:"name" db:"name"`
// Company the contact works for
Company string `json:"company" db:"company"`
// Phone number in international format
Phone string `json:"phone" db:"phone"`
}

View File

@@ -0,0 +1,74 @@
package repository
import (
"database/sql"
"fmt"
)
type baseRepository[T any] struct {
db *sql.DB
tableName string
}
func NewBaseRepository[T any](db *sql.DB, tableName string) *baseRepository[T] {
return &baseRepository[T]{
db: db,
tableName: tableName,
}
}
func (r *baseRepository[T]) BuildQuery(baseQuery string) string {
return fmt.Sprintf(baseQuery, r.tableName)
}
func (r *baseRepository[T]) GetDB() *sql.DB {
return r.db
}
func (r *baseRepository[T]) GetAll() ([]T, error) {
query := r.BuildQuery("SELECT * FROM %s ORDER BY id DESC")
rows, err := r.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
entities := make([]T, 0)
rowsErr := ScanRows(rows, &entities)
if rowsErr != nil {
return nil, err
}
return entities, nil
}
func (r *baseRepository[T]) GetByID(id int) (*T, error) {
var entity T
query := r.BuildQuery("SELECT * FROM %s WHERE id = ?")
row := r.db.QueryRow(
query,
id,
)
err := scanRow(row, &entity)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &entity, nil
}
func (r *baseRepository[T]) Delete(id int) (int64, error) {
query := r.BuildQuery("DELETE FROM %s WHERE id = ?")
res, err := r.db.Exec(query, id)
if err != nil {
return 0, err
}
return res.RowsAffected()
}

View File

@@ -0,0 +1,83 @@
package repository
import (
"database/sql"
"fmt"
"gitea.gabilandia.com/gabdlr/agenda-web-go/internal/models"
)
type ContactRepository struct {
baseRepository[models.Contact]
}
func NewContactRepository(db *sql.DB) Repository[models.Contact] {
return &ContactRepository{
baseRepository[models.Contact]{
db: db,
tableName: "contacts",
}}
}
func (r *ContactRepository) Create(contact *models.Contact) (int64, error) {
query := r.BuildQuery("INSERT INTO %s (name, company, phone) VALUES (?, ?, ?)")
result, err := r.db.Exec(
query,
contact.Name, contact.Company, contact.Phone,
)
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
contact.ID = int(id)
return id, nil
}
func (r *ContactRepository) Update(contact *models.Contact) error {
query := r.BuildQuery("UPDATE %s SET")
fieldsToUpdate := make([]string, 0, 4)
fields := make([]any, 0)
if contact.Name != "" {
fieldsToUpdate = append(fieldsToUpdate, "name")
fields = append(fields, &contact.Name)
}
if contact.Company != "" {
fieldsToUpdate = append(fieldsToUpdate, "company")
fields = append(fields, &contact.Company)
}
if contact.Phone != "" {
fieldsToUpdate = append(fieldsToUpdate, "phone")
fields = append(fields, &contact.Phone)
}
fields = append(fields, &contact.ID)
fieldsToUpdatelen := len(fieldsToUpdate)
for i, field := range fieldsToUpdate {
query += fmt.Sprintf(" %s = ?", field)
if i != fieldsToUpdatelen-1 {
query += ","
}
}
query += " WHERE id = ?"
_, err := r.db.Exec(query,
fields...,
)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,9 @@
package repository
type Repository[T any] interface {
Create(T *T) (int64, error)
Delete(id int) (int64, error)
GetAll() ([]T, error)
GetByID(id int) (*T, error)
Update(contact *T) error
}

View File

@@ -0,0 +1,55 @@
package repository
import (
"database/sql"
"fmt"
"reflect"
)
func scanRow(row *sql.Row, dest any) error {
destValue := reflect.ValueOf(dest).Elem()
fields := make([]any, destValue.NumField())
for i := 0; i < destValue.NumField(); i++ {
fields[i] = destValue.Field(i).Addr().Interface()
}
return row.Scan(fields...)
}
func ScanRows(rows *sql.Rows, destSlice any) error {
sliceValue := reflect.ValueOf(destSlice)
if sliceValue.Kind() != reflect.Pointer || sliceValue.Elem().Kind() != reflect.Slice {
return fmt.Errorf("destSlice must be a pointer to a slice")
}
sliceElem := sliceValue.Elem()
structType := sliceElem.Type().Elem()
for rows.Next() {
newStruct := reflect.New(structType).Elem()
fields := make([]any, newStruct.NumField())
for i := 0; i < newStruct.NumField(); i++ {
fields[i] = newStruct.Field(i).Addr().Interface()
}
if err := rows.Scan(fields...); err != nil {
return err
}
sliceElem.Set(reflect.Append(sliceElem, newStruct))
}
return rows.Err()
}
func GetStructFieldsPtr(object any) []any {
destValue := reflect.ValueOf(object).Elem()
fields := make([]any, destValue.NumField())
for i := 0; i < destValue.NumField(); i++ {
fields[i] = destValue.Field(i).Addr().Interface()
}
return fields
}