chore: coded post req for create posts on db
All checks were successful
Backend ci / build (push) Successful in 7m43s

This commit is contained in:
d3m0k1d
2026-02-03 23:21:01 +03:00
parent 44406b02d3
commit 9b9d45e0a6
14 changed files with 254 additions and 67 deletions

1
backend/.gitignore vendored
View File

@@ -1 +1,2 @@
/bin /bin
/data

View File

@@ -5,6 +5,7 @@ WORKDIR /app
COPY . . COPY . .
ENV CGO_ENABLED=0 ENV CGO_ENABLED=0
ENV GIN_MODE=release
RUN go mod tidy RUN go mod tidy
RUN go build -ldflags "-s -w" -o backend ./cmd/main.go RUN go build -ldflags "-s -w" -o backend ./cmd/main.go

View File

@@ -3,8 +3,8 @@ package main
import ( import (
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/docs" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/docs"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/handlers" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/handlers"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
@@ -13,6 +13,22 @@ import (
func main() { func main() {
log := logger.New(false) log := logger.New(false)
db, err := storage.OpenSession()
if err != nil {
log.Error("Failed to open database", "error", err)
panic(err)
}
defer func() {
err := db.Close()
if err != nil {
log.Error("Failed to close database", "error", err)
}
}()
if err := storage.CreateTables(db); err != nil {
log.Error("Failed to create tables", "error", err)
panic(err)
}
router := gin.Default() router := gin.Default()
// Swagger config // Swagger config
@@ -20,13 +36,14 @@ func main() {
docs.SwaggerInfo.Title = "d3m0k1d.ru API" docs.SwaggerInfo.Title = "d3m0k1d.ru API"
docs.SwaggerInfo.Version = "1.0" docs.SwaggerInfo.Version = "1.0"
docs.SwaggerInfo.Description = "API for d3m0k1d.ru" docs.SwaggerInfo.Description = "API for d3m0k1d.ru"
docs.SwaggerInfo.Host = "d3m0k1d.ru" docs.SwaggerInfo.Schemes = []string{"http"}
docs.SwaggerInfo.Schemes = []string{"https"}
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
handlers.Register(router)
handlers.Register(router, db)
log.Info("Starting server on :8080...") log.Info("Starting server on :8080...")
if err := router.Run(":8080"); err != nil { if err := router.Run(":8080"); err != nil {
log.Error("Error starting server: " + err.Error()) log.Error("Error starting server", "error", err)
panic(err) panic(err)
} }
} }

View File

@@ -15,7 +15,7 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/api/v1/posts": { "/posts": {
"get": { "get": {
"description": "Get all posts", "description": "Get all posts",
"consumes": [ "consumes": [
@@ -34,14 +34,14 @@ const docTemplate = `{
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
} }
} }
} }
} }
}, },
"post": { "post": {
"description": "Create post", "description": "Create new post",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -52,17 +52,34 @@ const docTemplate = `{
"posts" "posts"
], ],
"summary": "Create post", "summary": "Create post",
"parameters": [
{
"description": "Post data",
"name": "post",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
} }
} }
} }
} }
}, },
"/api/v1/posts/{id}": { "/posts/{id}": {
"get": { "get": {
"description": "Get post by id", "description": "Get post by id",
"consumes": [ "consumes": [
@@ -79,7 +96,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
} }
} }
} }
@@ -100,7 +117,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post"
} }
} }
} }
@@ -121,7 +138,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post"
} }
} }
} }
@@ -129,7 +146,11 @@ const docTemplate = `{
} }
}, },
"definitions": { "definitions": {
"storage.Post": { "gin.H": {
"type": "object",
"additionalProperties": {}
},
"gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post": {
"type": "object", "type": "object",
"properties": { "properties": {
"content": { "content": {
@@ -145,6 +166,28 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
} }
},
"gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"title": {
"type": "string"
}
}
} }
} }
}` }`

View File

@@ -4,7 +4,7 @@
"contact": {} "contact": {}
}, },
"paths": { "paths": {
"/api/v1/posts": { "/posts": {
"get": { "get": {
"description": "Get all posts", "description": "Get all posts",
"consumes": [ "consumes": [
@@ -23,14 +23,14 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
} }
} }
} }
} }
}, },
"post": { "post": {
"description": "Create post", "description": "Create new post",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -41,17 +41,34 @@
"posts" "posts"
], ],
"summary": "Create post", "summary": "Create post",
"parameters": [
{
"description": "Post data",
"name": "post",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/gin.H"
} }
} }
} }
} }
}, },
"/api/v1/posts/{id}": { "/posts/{id}": {
"get": { "get": {
"description": "Get post by id", "description": "Get post by id",
"consumes": [ "consumes": [
@@ -68,7 +85,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq"
} }
} }
} }
@@ -89,7 +106,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post"
} }
} }
} }
@@ -110,7 +127,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/storage.Post" "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post"
} }
} }
} }
@@ -118,7 +135,11 @@
} }
}, },
"definitions": { "definitions": {
"storage.Post": { "gin.H": {
"type": "object",
"additionalProperties": {}
},
"gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post": {
"type": "object", "type": "object",
"properties": { "properties": {
"content": { "content": {
@@ -134,6 +155,28 @@
"type": "string" "type": "string"
} }
} }
},
"gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"title": {
"type": "string"
}
}
} }
} }
} }

View File

@@ -1,5 +1,8 @@
definitions: definitions:
storage.Post: gin.H:
additionalProperties: {}
type: object
gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post:
properties: properties:
content: content:
type: string type: string
@@ -10,10 +13,24 @@ definitions:
title: title:
type: string type: string
type: object type: object
gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate:
properties:
content:
type: string
title:
type: string
type: object
gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq:
properties:
content:
type: string
title:
type: string
type: object
info: info:
contact: {} contact: {}
paths: paths:
/api/v1/posts: /posts:
get: get:
consumes: consumes:
- application/json - application/json
@@ -25,7 +42,7 @@ paths:
description: OK description: OK
schema: schema:
items: items:
$ref: '#/definitions/storage.Post' $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq'
type: array type: array
summary: Get all posts summary: Get all posts
tags: tags:
@@ -33,18 +50,29 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: Create post description: Create new post
parameters:
- description: Post data
in: body
name: post
required: true
schema:
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostCreate'
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/storage.Post' $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq'
"400":
description: Bad Request
schema:
$ref: '#/definitions/gin.H'
summary: Create post summary: Create post
tags: tags:
- posts - posts
/api/v1/posts/{id}: /posts/{id}:
delete: delete:
consumes: consumes:
- application/json - application/json
@@ -55,7 +83,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/storage.Post' $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post'
summary: Delete post summary: Delete post
tags: tags:
- posts - posts
@@ -69,7 +97,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/storage.Post' $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.PostReq'
summary: Get post by id summary: Get post by id
tags: tags:
- posts - posts
@@ -83,7 +111,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/storage.Post' $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_d3m0k1d_ru_backend_internal_storage.Post'
summary: Update post summary: Update post
tags: tags:
- posts - posts

View File

@@ -2,9 +2,19 @@ package handlers
import ( import (
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger" "gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/logger"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/storage"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type PostHandlers struct {
repo repositories.PostRepository
}
func NewPostHandler(repo repositories.PostRepository) *PostHandlers {
return &PostHandlers{repo: repo}
}
var log = logger.New(false) var log = logger.New(false)
// GetPosts godoc // GetPosts godoc
@@ -14,9 +24,16 @@ var log = logger.New(false)
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} []storage.PostReq // @Success 200 {object} []storage.PostReq
// @Router /api/v1/posts [get] // @Router /posts [get]
func GetPosts(c *gin.Context) { func (h *PostHandlers) GetPosts(c *gin.Context) {
log.Info("GetPosts") var result []storage.PostReq
result, err := h.repo.GetAll(c.Request.Context())
if err != nil {
log.Error("error request: " + err.Error())
c.Status(500)
}
log.Info("200 OK GET /posts")
c.JSON(200, result)
} }
// GetPost godoc // GetPost godoc
@@ -26,21 +43,40 @@ func GetPosts(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} storage.PostReq // @Success 200 {object} storage.PostReq
// @Router /api/v1/posts/{id} [get] // @Router /posts/{id} [get]
func GetPost(c *gin.Context) { func GetPost(c *gin.Context) {
log.Info("GetPost") log.Info("GetPost")
} }
// CreatePost godoc // CreatePost godoc
// @Summary Create post // @Summary Create post
// @Description Create post // @Description Create new post
// @Tags posts // @Tags posts
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} storage.Post // @Param post body storage.PostCreate true "Post data"
// @Router /api/v1/posts [post] // @Success 200 {object} storage.PostReq
func CreatePost(c *gin.Context) { // @Failure 400 {object} gin.H
log.Info("CreatePost") // @Router /posts [post]
func (h *PostHandlers) CreatePost(c *gin.Context) {
var req storage.PostCreate
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
post := storage.Post{
Title: req.Title,
Content: req.Content,
}
if err := h.repo.Create(c.Request.Context(), post); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, post)
} }
// UpdatePost godoc // UpdatePost godoc
@@ -50,7 +86,7 @@ func CreatePost(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} storage.Post // @Success 200 {object} storage.Post
// @Router /api/v1/posts/{id} [put] // @Router /posts/{id} [put]
func UpdatePost(c *gin.Context) { func UpdatePost(c *gin.Context) {
log.Info("UpdatePost") log.Info("UpdatePost")
} }
@@ -62,7 +98,7 @@ func UpdatePost(c *gin.Context) {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} storage.Post // @Success 200 {object} storage.Post
// @Router /api/v1/posts/{id} [delete] // @Router /posts/{id} [delete]
func DeletePost(c *gin.Context) { func DeletePost(c *gin.Context) {
log.Info("DeletePost") log.Info("DeletePost")
} }

View File

@@ -1,16 +1,20 @@
package handlers package handlers
import ( import (
"database/sql"
"gitea.d3m0k1d.ru/d3m0k1d/d3m0k1d.ru/backend/internal/repositories"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func Register(router *gin.Engine) { func Register(router *gin.Engine, db *sql.DB) {
handler_posts := NewPostHandler(repositories.NewPostRepository(db))
v1 := router.Group("api/v1") v1 := router.Group("api/v1")
posts := v1.Group("posts") posts := v1.Group("posts")
{ {
posts.GET("/", GetPosts) posts.GET("/", handler_posts.GetPosts)
posts.GET("/:id", GetPost) posts.GET("/:id", GetPost)
posts.POST("/", CreatePost) posts.POST("/", handler_posts.CreatePost)
posts.PUT("/:id", UpdatePost) posts.PUT("/:id", UpdatePost)
posts.DELETE("/:id", DeletePost) posts.DELETE("/:id", DeletePost)
} }

View File

@@ -14,13 +14,13 @@ type Logger struct {
} }
func New(debug bool) *Logger { func New(debug bool) *Logger {
logDir := "/var/log/banforge" logDir := "/var/log/backend"
if err := os.MkdirAll(logDir, 0750); err != nil { if err := os.MkdirAll(logDir, 0750); err != nil {
return nil return nil
} }
fileWriter := &lumberjack.Logger{ fileWriter := &lumberjack.Logger{
Filename: filepath.Join(logDir, "banforge.log"), Filename: filepath.Join(logDir, "backend.log"),
MaxSize: 500, MaxSize: 500,
MaxBackups: 3, MaxBackups: 3,
MaxAge: 28, MaxAge: 28,

View File

@@ -58,7 +58,19 @@ func (p *postRepository) GetByID(ctx context.Context, id int) (storage.PostReq,
} }
func (p *postRepository) Create(ctx context.Context, post storage.Post) error { func (p *postRepository) Create(ctx context.Context, post storage.Post) error {
query, err := p.db.Exec(
"INSERT INTO posts(title, content) VALUES(?, ?)",
post.Title,
post.Content,
)
if err != nil {
return err
}
id, err := query.LastInsertId()
if err != nil {
return err
}
p.logger.Info("Post created:", "id", id)
return nil return nil
} }

View File

@@ -12,24 +12,21 @@ var db_path = os.Getenv(
"DB_PATH", "DB_PATH",
) + "?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON" ) + "?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=2000&_foreign_keys=ON"
func CreateTables() error { func CreateTables(db *sql.DB) error {
logger := logger.New(false) logger := logger.New(false)
db, err := sql.Open("sqlite", db_path) _, err := db.Exec(Migrations)
if err != nil {
logger.Error(err.Error())
return err
}
_, err = db.Exec(Migrations)
if err != nil { if err != nil {
logger.Error(err.Error()) logger.Error(err.Error())
return err return err
} }
defer func() {
err = db.Close()
if err != nil {
logger.Error(err.Error())
}
}()
return nil return nil
} }
func OpenSession() (*sql.DB, error) {
db, err := sql.Open("sqlite", db_path)
if err != nil {
return nil, err
}
return db, nil
}

View File

@@ -2,9 +2,9 @@ package storage
const Migrations = ` const Migrations = `
CREATE TABLE IF NOT EXISTS posts( CREATE TABLE IF NOT EXISTS posts(
id INTEGER PRIMARY KEY AUTOINCREMENT id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL title TEXT NOT NULL,
content TEXT NOT NULL content TEXT NOT NULL,
CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP
); );
` `

View File

@@ -11,3 +11,8 @@ type PostReq struct {
Title string `json:"title"` Title string `json:"title"`
Content string `json:"content"` Content string `json:"content"`
} }
type PostCreate struct {
Title string `json:"title"`
Content string `json:"content"`
}