Compare commits
22 Commits
8adf1bd4c8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 503016d5b3 | |||
| cbc2646c3f | |||
| 027101081b | |||
| a76999fc71 | |||
| 30398245e2 | |||
| 74c1e6721e | |||
| bacc3f533a | |||
| 65838bac74 | |||
| 01dc6bb462 | |||
| a82adb90c2 | |||
| 6f419ece3d | |||
| 56209011ec | |||
| 99d035c915 | |||
| 01a7f33054 | |||
| 92f13fba22 | |||
| 3b1b05d5a6 | |||
| 4a28259dc8 | |||
| af511203a4 | |||
| e06c30635e | |||
| ffdaa22aa3 | |||
| 1f4711edb6 | |||
| de58856289 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
set_dev_env.sh
|
||||||
11
AI.md
Normal file
11
AI.md
Normal 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
20
Dockerfile
Normal 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
27
README.md
Normal 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
41
cmd/server/main.go
Normal 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
23
docker-compose.yml
Normal 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
380
docs/docs.go
Normal 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
356
docs/swagger.json
Normal 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
231
docs/swagger.yaml
Normal 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
29
go.mod
@@ -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
83
go.sum
Normal 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=
|
||||||
125
internal/database/database.go
Normal file
125
internal/database/database.go
Normal 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
|
||||||
|
}
|
||||||
47
internal/handler/base_handler.go
Normal file
47
internal/handler/base_handler.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
internal/handler/contact_handler.go
Normal file
198
internal/handler/contact_handler.go
Normal 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)
|
||||||
|
}
|
||||||
88
internal/handler/errors.go
Normal file
88
internal/handler/errors.go
Normal 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))
|
||||||
|
}
|
||||||
16
internal/handler/health_handler.go
Normal file
16
internal/handler/health_handler.go
Normal 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"})
|
||||||
|
}
|
||||||
65
internal/handler/response.go
Normal file
65
internal/handler/response.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
19
internal/middleware/cors.go
Normal file
19
internal/middleware/cors.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
13
internal/models/contact.go
Normal file
13
internal/models/contact.go
Normal 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"`
|
||||||
|
}
|
||||||
74
internal/repository/base_repository.go
Normal file
74
internal/repository/base_repository.go
Normal 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()
|
||||||
|
}
|
||||||
83
internal/repository/contact_repository.go
Normal file
83
internal/repository/contact_repository.go
Normal 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
|
||||||
|
}
|
||||||
9
internal/repository/interfaces.go
Normal file
9
internal/repository/interfaces.go
Normal 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
|
||||||
|
}
|
||||||
55
internal/repository/scanner_helper.go
Normal file
55
internal/repository/scanner_helper.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user