feat: add components management functionality

This commit is contained in:
Tran Anh Tuan
2026-05-11 10:35:38 +07:00
parent 50564e9b78
commit bf20286f04
15 changed files with 1662 additions and 1 deletions

View File

@@ -17,6 +17,7 @@ const (
API_GROUP_SHELF = "/shelves"
API_GROUP_CONTAINER = "/containers"
API_GROUP_COMPONENT_TYPE = "/component-types"
API_GROUP_COMPONENT = "/components"
)
const (

36
db/queries/component.sql Normal file
View File

@@ -0,0 +1,36 @@
-- name: GetComponentByID :one
SELECT * FROM components
WHERE id = sqlc.arg(id);
-- name: ListComponents :many
SELECT * FROM components
ORDER BY created_at DESC;
-- name: CreateComponent :one
INSERT INTO components (component_type_id,name, description,unit,min_quantity,metadata, created_at)
VALUES (
sqlc.arg(component_type_id),
sqlc.arg(name),
sqlc.arg(description),
sqlc.arg(unit),
sqlc.arg(min_quantity),
sqlc.arg(metadata),
sqlc.arg(created_at)
)
RETURNING *;
-- name: UpdateComponent :one
UPDATE components
SET name = CASE WHEN sqlc.arg(name) = '' THEN name ELSE sqlc.arg(name) END,
component_type_id = coalesce(sqlc.arg(component_type_id), component_type_id),
description = coalesce(sqlc.arg(description), description),
unit = coalesce(sqlc.arg(unit), unit),
min_quantity = coalesce(sqlc.arg(min_quantity), min_quantity),
metadata = coalesce(sqlc.arg(metadata), metadata),
updated_at = sqlc.arg(updated_at)
WHERE id = sqlc.arg(id)
RETURNING *;
-- name: DeleteComponent :execrows
DELETE FROM components
WHERE id = sqlc.arg(id);

View File

@@ -288,6 +288,279 @@ const docTemplate = `{
}
}
},
"/api/v1/components": {
"get": {
"description": "Retrieve a list of all components ordered by creation date",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "List all components",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Component"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a new component with the provided details",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "Create a new component",
"parameters": [
{
"description": "Component request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.CreateComponentRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.CreateComponentResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/api/v1/components/{id}": {
"get": {
"description": "Retrieve a single component using its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "Get component by ID",
"parameters": [
{
"type": "integer",
"description": "Component ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Component"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"put": {
"description": "Update an existing component by its ID. Only non-empty fields will be updated.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "Update component",
"parameters": [
{
"type": "integer",
"description": "Component ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Component request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.UpdateComponentRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.UpdateComponentResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete a component by its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "Delete component",
"parameters": [
{
"type": "integer",
"description": "Component ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.SuccessResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/auth/register": {
"post": {
"description": "Register with email, username and password",
@@ -1762,6 +2035,44 @@ const docTemplate = `{
}
}
},
"models.Component": {
"type": "object",
"properties": {
"componentTypeId": {
"type": "integer"
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"totalQuantity": {
"type": "integer"
},
"unit": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"models.ComponentType": {
"type": "object",
"properties": {
@@ -1936,6 +2247,38 @@ const docTemplate = `{
}
}
},
"requests.CreateComponentRequest": {
"type": "object",
"required": [
"componentTypeId",
"minQuantity",
"name",
"unit"
],
"properties": {
"componentTypeId": {
"type": "integer"
},
"description": {
"type": "string"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"unit": {
"type": "string"
}
}
},
"requests.CreateComponentTypeRequest": {
"type": "object",
"required": [
@@ -2056,6 +2399,32 @@ const docTemplate = `{
}
}
},
"requests.UpdateComponentRequest": {
"type": "object",
"properties": {
"componentTypeId": {
"type": "integer"
},
"description": {
"type": "string"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"unit": {
"type": "string"
}
}
},
"requests.UpdateComponentTypeRequest": {
"type": "object",
"properties": {
@@ -2184,6 +2553,14 @@ const docTemplate = `{
}
}
},
"responses.CreateComponentResponse": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
},
"responses.CreateComponentTypeResponse": {
"type": "object",
"properties": {
@@ -2241,6 +2618,29 @@ const docTemplate = `{
}
}
},
"responses.UpdateComponentResponse": {
"type": "object",
"properties": {
"componentTypeId": {
"type": "integer"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"unit": {
"type": "string"
}
}
},
"responses.UpdateComponentTypeResponse": {
"type": "object",
"properties": {

View File

@@ -282,6 +282,279 @@
}
}
},
"/api/v1/components": {
"get": {
"description": "Retrieve a list of all components ordered by creation date",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "List all components",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Component"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a new component with the provided details",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "Create a new component",
"parameters": [
{
"description": "Component request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.CreateComponentRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.CreateComponentResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/api/v1/components/{id}": {
"get": {
"description": "Retrieve a single component using its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "Get component by ID",
"parameters": [
{
"type": "integer",
"description": "Component ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Component"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"put": {
"description": "Update an existing component by its ID. Only non-empty fields will be updated.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "Update component",
"parameters": [
{
"type": "integer",
"description": "Component ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Component request body",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.UpdateComponentRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.SuccessResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/responses.UpdateComponentResponse"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete a component by its unique identifier",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"component"
],
"summary": "Delete component",
"parameters": [
{
"type": "integer",
"description": "Component ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.SuccessResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/auth/register": {
"post": {
"description": "Register with email, username and password",
@@ -1756,6 +2029,44 @@
}
}
},
"models.Component": {
"type": "object",
"properties": {
"componentTypeId": {
"type": "integer"
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"totalQuantity": {
"type": "integer"
},
"unit": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
},
"models.ComponentType": {
"type": "object",
"properties": {
@@ -1930,6 +2241,38 @@
}
}
},
"requests.CreateComponentRequest": {
"type": "object",
"required": [
"componentTypeId",
"minQuantity",
"name",
"unit"
],
"properties": {
"componentTypeId": {
"type": "integer"
},
"description": {
"type": "string"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"unit": {
"type": "string"
}
}
},
"requests.CreateComponentTypeRequest": {
"type": "object",
"required": [
@@ -2050,6 +2393,32 @@
}
}
},
"requests.UpdateComponentRequest": {
"type": "object",
"properties": {
"componentTypeId": {
"type": "integer"
},
"description": {
"type": "string"
},
"metadata": {
"type": "array",
"items": {
"type": "integer"
}
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"unit": {
"type": "string"
}
}
},
"requests.UpdateComponentTypeRequest": {
"type": "object",
"properties": {
@@ -2178,6 +2547,14 @@
}
}
},
"responses.CreateComponentResponse": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
},
"responses.CreateComponentTypeResponse": {
"type": "object",
"properties": {
@@ -2235,6 +2612,29 @@
}
}
},
"responses.UpdateComponentResponse": {
"type": "object",
"properties": {
"componentTypeId": {
"type": "integer"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"minQuantity": {
"type": "integer"
},
"name": {
"type": "string"
},
"unit": {
"type": "string"
}
}
},
"responses.UpdateComponentTypeResponse": {
"type": "object",
"properties": {

View File

@@ -15,6 +15,31 @@ definitions:
updatedAt:
type: string
type: object
models.Component:
properties:
componentTypeId:
type: integer
createdAt:
type: string
description:
type: string
id:
type: integer
metadata:
items:
type: integer
type: array
minQuantity:
type: integer
name:
type: string
totalQuantity:
type: integer
unit:
type: string
updatedAt:
type: string
type: object
models.ComponentType:
properties:
createdAt:
@@ -130,6 +155,28 @@ definitions:
- name
- roomId
type: object
requests.CreateComponentRequest:
properties:
componentTypeId:
type: integer
description:
type: string
metadata:
items:
type: integer
type: array
minQuantity:
type: integer
name:
type: string
unit:
type: string
required:
- componentTypeId
- minQuantity
- name
- unit
type: object
requests.CreateComponentTypeRequest:
properties:
description:
@@ -210,6 +257,23 @@ definitions:
name:
type: string
type: object
requests.UpdateComponentRequest:
properties:
componentTypeId:
type: integer
description:
type: string
metadata:
items:
type: integer
type: array
minQuantity:
type: integer
name:
type: string
unit:
type: string
type: object
requests.UpdateComponentTypeRequest:
properties:
description:
@@ -293,6 +357,11 @@ definitions:
id:
type: integer
type: object
responses.CreateComponentResponse:
properties:
id:
type: integer
type: object
responses.CreateComponentTypeResponse:
properties:
id:
@@ -329,6 +398,21 @@ definitions:
roomId:
type: integer
type: object
responses.UpdateComponentResponse:
properties:
componentTypeId:
type: integer
description:
type: string
id:
type: integer
minQuantity:
type: integer
name:
type: string
unit:
type: string
type: object
responses.UpdateComponentTypeResponse:
properties:
description:
@@ -569,6 +653,176 @@ paths:
summary: Update component type
tags:
- component-type
/api/v1/components:
get:
consumes:
- application/json
description: Retrieve a list of all components ordered by creation date
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
items:
$ref: '#/definitions/models.Component'
type: array
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: List all components
tags:
- component
post:
consumes:
- application/json
description: Create a new component with the provided details
parameters:
- description: Component request body
in: body
name: body
required: true
schema:
$ref: '#/definitions/requests.CreateComponentRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/responses.CreateComponentResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Create a new component
tags:
- component
/api/v1/components/{id}:
delete:
consumes:
- application/json
description: Delete a component by its unique identifier
parameters:
- description: Component ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.SuccessResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Delete component
tags:
- component
get:
consumes:
- application/json
description: Retrieve a single component using its unique identifier
parameters:
- description: Component ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/models.Component'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Get component by ID
tags:
- component
put:
consumes:
- application/json
description: Update an existing component by its ID. Only non-empty fields will
be updated.
parameters:
- description: Component ID
in: path
name: id
required: true
type: integer
- description: Component request body
in: body
name: body
required: true
schema:
$ref: '#/definitions/requests.UpdateComponentRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.SuccessResponse'
- properties:
data:
$ref: '#/definitions/responses.UpdateComponentResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
summary: Update component
tags:
- component
/auth/register:
post:
consumes:

View File

@@ -0,0 +1,60 @@
package mapper
import (
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
)
func ToDomainComponent(r db.Component) *models.Component {
return &models.Component{
ID: r.ID,
ComponentTypeID: r.ComponentTypeID,
Name: r.Name,
Description: r.Description.String,
Unit: r.Unit,
TotalQuantity: r.TotalQuantity,
MinQuantity: r.MinQuantity,
Metadata: json.RawMessage(r.Metadata),
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
}
func ToModelComponent(r *models.Component) *db.CreateComponentParams {
return &db.CreateComponentParams{
ComponentTypeID: r.ComponentTypeID,
Name: r.Name,
Description: pgtype.Text{
String: r.Description,
Valid: r.Description != "",
},
Unit: r.Unit,
MinQuantity: r.MinQuantity,
Metadata: []byte(r.Metadata),
CreatedAt: r.CreatedAt,
}
}
func ToUpdateModelComponent(r *models.Component) *db.UpdateComponentParams {
var metadata []byte
if len(r.Metadata) > 0 {
metadata = []byte(r.Metadata)
}
return &db.UpdateComponentParams{
ComponentTypeID: r.ComponentTypeID,
Name: r.Name,
Description: pgtype.Text{
String: r.Description,
Valid: r.Description != "",
},
Unit: r.Unit,
MinQuantity: r.MinQuantity,
Metadata: metadata,
UpdatedAt: r.UpdatedAt,
ID: r.ID,
}
}

View File

@@ -0,0 +1,19 @@
package models
import (
"encoding/json"
"time"
)
type Component struct {
ID int64 `json:"id"`
ComponentTypeID int64 `json:"componentTypeId"`
Name string `json:"name"`
Description string `json:"description"`
Unit string `json:"unit"`
TotalQuantity int32 `json:"totalQuantity"`
MinQuantity int32 `json:"minQuantity"`
Metadata json.RawMessage `json:"metadata"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@@ -0,0 +1,21 @@
package requests
import "encoding/json"
type CreateComponentRequest struct {
ComponentTypeID int64 `json:"componentTypeId" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Unit string `json:"unit" binding:"required"`
MinQuantity int32 `json:"minQuantity" binding:"required"`
Metadata json.RawMessage `json:"metadata"`
}
type UpdateComponentRequest struct {
ComponentTypeID int64 `json:"componentTypeId"`
Name string `json:"name"`
Description string `json:"description"`
Unit string `json:"unit"`
MinQuantity int32 `json:"minQuantity"`
Metadata json.RawMessage `json:"metadata"`
}

View File

@@ -0,0 +1,14 @@
package responses
type CreateComponentResponse struct {
ID int64 `json:"id"`
}
type UpdateComponentResponse struct {
ID int64 `json:"id"`
ComponentTypeID int64 `json:"componentTypeId"`
Name string `json:"name"`
Description string `json:"description"`
Unit string `json:"unit"`
MinQuantity int32 `json:"minQuantity"`
}

View File

@@ -0,0 +1,55 @@
package repositories
import (
"context"
"wm-backend/internal/mapper"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/rs/zerolog/log"
)
func CreateComponent(ctx context.Context, queries *db.Queries, body models.Component) (models.Component, error) {
result, err := queries.CreateComponent(ctx, *mapper.ToModelComponent(&body))
if err != nil {
return models.Component{}, err
}
return *mapper.ToDomainComponent(result), nil
}
func GetComponentByID(ctx context.Context, queries *db.Queries, id int64) (models.Component, error) {
result, err := queries.GetComponentByID(ctx, id)
if err != nil {
return models.Component{}, err
}
return *mapper.ToDomainComponent(result), nil
}
func ListComponents(ctx context.Context, queries *db.Queries) ([]models.Component, error) {
results, err := queries.ListComponents(ctx)
if err != nil {
return nil, err
}
var items []models.Component
for _, r := range results {
items = append(items, *mapper.ToDomainComponent(r))
}
return items, nil
}
func UpdateComponent(ctx context.Context, queries *db.Queries, body models.Component) (models.Component, error) {
result, err := queries.UpdateComponent(ctx, *mapper.ToUpdateModelComponent(&body))
log.Info().Any("component", result).Err(err).Msg("Updating component")
if err != nil {
return models.Component{}, err
}
return *mapper.ToDomainComponent(result), nil
}
func DeleteComponent(ctx context.Context, queries *db.Queries, id int64) (int64, error) {
rowsAffected, err := queries.DeleteComponent(ctx, id)
if err != nil {
return rowsAffected, err
}
return rowsAffected, nil
}

View File

@@ -82,6 +82,15 @@ func NewRouter() *gin.Engine {
componentType.PUT("/:id", utils.AsyncHandler(services.ComponentTypeUpdate))
componentType.DELETE("/:id", utils.AsyncHandler(services.ComponentTypeDelete))
}
component := v1.Group(constants.API_GROUP_COMPONENT)
{
component.GET("", utils.AsyncHandler(services.ComponentList))
component.GET("/:id", utils.AsyncHandler(services.ComponentGetByID))
component.POST("", utils.AsyncHandler(services.ComponentCreate))
component.PUT("/:id", utils.AsyncHandler(services.ComponentUpdate))
component.DELETE("/:id", utils.AsyncHandler(services.ComponentDelete))
}
}
r.GET(constants.API_PATH_PING, services.PingHandler)

View File

@@ -0,0 +1,201 @@
package services
import (
"net/http"
"strconv"
"time"
"wm-backend/global"
"wm-backend/internal/models"
"wm-backend/internal/models/requests"
"wm-backend/internal/models/responses"
"wm-backend/internal/repositories"
"wm-backend/pkg/helper"
"wm-backend/response"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
// ComponentCreate creates a new component.
// It validates the request body and creates the component in the database.
//
// @Summary Create a new component
// @Description Create a new component with the provided details
// @Tags component
// @Accept json
// @Produce json
// @Param body body requests.CreateComponentRequest true "Component request body"
// @Success 201 {object} response.SuccessResponse{data=responses.CreateComponentResponse}
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/components [post]
func ComponentCreate(c *gin.Context) error {
requestBody := requests.CreateComponentRequest{}
if helper.IsShouldBindJSON(c, &requestBody) {
return nil
}
componentModel := &models.Component{
ComponentTypeID: requestBody.ComponentTypeID,
Name: requestBody.Name,
Description: requestBody.Description,
Unit: requestBody.Unit,
MinQuantity: requestBody.MinQuantity,
Metadata: requestBody.Metadata,
CreatedAt: time.Now(),
}
component, err := repositories.CreateComponent(c.Request.Context(), global.Queries, *componentModel)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to create component")
return nil
}
response.Created(c, "Component created successfully", &responses.CreateComponentResponse{
ID: component.ID,
})
return nil
}
// ComponentGetByID retrieves a single component by its ID.
//
// @Summary Get component by ID
// @Description Retrieve a single component using its unique identifier
// @Tags component
// @Accept json
// @Produce json
// @Param id path int true "Component ID"
// @Success 200 {object} response.SuccessResponse{data=models.Component}
// @Failure 400 {object} response.ErrorResponse
// @Failure 404 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/components/{id} [get]
func ComponentGetByID(c *gin.Context) error {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
return nil
}
component, err := repositories.GetComponentByID(c.Request.Context(), global.Queries, id)
if err != nil {
response.NotFoundError(c, http.StatusNotFound, "Component not found")
return nil
}
response.Ok(c, "Success", component)
return nil
}
// ComponentList retrieves all components.
//
// @Summary List all components
// @Description Retrieve a list of all components ordered by creation date
// @Tags component
// @Accept json
// @Produce json
// @Success 200 {object} response.SuccessResponse{data=[]models.Component}
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/components [get]
func ComponentList(c *gin.Context) error {
components, err := repositories.ListComponents(c.Request.Context(), global.Queries)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to list components")
return nil
}
response.Ok(c, "Success", components)
return nil
}
// ComponentUpdate updates an existing component by its ID.
// It validates the request body, fetches the existing record,
// merges non-empty fields from the request, and updates the component in the database.
//
// @Summary Update component
// @Description Update an existing component by its ID. Only non-empty fields will be updated.
// @Tags component
// @Accept json
// @Produce json
// @Param id path int true "Component ID"
// @Param body body requests.UpdateComponentRequest true "Component request body"
// @Success 200 {object} response.SuccessResponse{data=responses.UpdateComponentResponse}
// @Failure 400 {object} response.ErrorResponse
// @Failure 404 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/components/{id} [put]
func ComponentUpdate(c *gin.Context) error {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
return nil
}
requestBody := requests.UpdateComponentRequest{}
if helper.IsShouldBindJSON(c, &requestBody) {
return nil
}
existing, err := repositories.GetComponentByID(c.Request.Context(), global.Queries, id)
if err != nil {
response.NotFoundError(c, http.StatusNotFound, "Component not found")
return nil
}
if requestBody.ComponentTypeID != 0 {
existing.ComponentTypeID = requestBody.ComponentTypeID
}
if requestBody.Name != "" {
existing.Name = requestBody.Name
}
if requestBody.Description != "" {
existing.Description = requestBody.Description
}
if requestBody.Unit != "" {
existing.Unit = requestBody.Unit
}
if requestBody.MinQuantity != 0 {
existing.MinQuantity = requestBody.MinQuantity
}
if len(requestBody.Metadata) > 0 {
existing.Metadata = requestBody.Metadata
}
existing.UpdatedAt = time.Now()
component, err := repositories.UpdateComponent(c.Request.Context(), global.Queries, existing)
if err != nil {
response.InternalServerError(c, http.StatusInternalServerError, "Failed to update component")
return nil
}
response.Ok(c, "Component updated successfully", &responses.UpdateComponentResponse{
ID: component.ID,
ComponentTypeID: component.ComponentTypeID,
Name: component.Name,
Description: component.Description,
Unit: component.Unit,
MinQuantity: component.MinQuantity,
})
return nil
}
// ComponentDelete deletes a component by its ID.
//
// @Summary Delete component
// @Description Delete a component by its unique identifier
// @Tags component
// @Accept json
// @Produce json
// @Param id path int true "Component ID"
// @Success 200 {object} response.SuccessResponse
// @Failure 400 {object} response.ErrorResponse
// @Failure 500 {object} response.ErrorResponse
// @Router /api/v1/components/{id} [delete]
func ComponentDelete(c *gin.Context) error {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
return nil
}
rowsAffected, err := repositories.DeleteComponent(c.Request.Context(), global.Queries, id)
if err != nil {
log.Error().Err(err).Msgf("Failed to delete component with ID: %d", id)
response.InternalServerError(c, http.StatusInternalServerError, "Failed to delete component")
return nil
}
if rowsAffected == 0 {
response.NotFoundError(c, http.StatusNotFound, "Component not found")
return nil
}
response.Ok(c, "Delete Success", nil)
return nil
}

186
sqlc_gen/component.sql.go Normal file
View File

@@ -0,0 +1,186 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: component.sql
package db
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
const createComponent = `-- name: CreateComponent :one
INSERT INTO components (component_type_id,name, description,unit,min_quantity,metadata, created_at)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
)
RETURNING id, component_type_id, name, description, unit, total_quantity, min_quantity, metadata, created_at, updated_at
`
type CreateComponentParams struct {
ComponentTypeID int64 `db:"component_type_id" json:"componentTypeId"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Unit string `db:"unit" json:"unit"`
MinQuantity int32 `db:"min_quantity" json:"minQuantity"`
Metadata []byte `db:"metadata" json:"metadata"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
}
func (q *Queries) CreateComponent(ctx context.Context, arg CreateComponentParams) (Component, error) {
row := q.db.QueryRow(ctx, createComponent,
arg.ComponentTypeID,
arg.Name,
arg.Description,
arg.Unit,
arg.MinQuantity,
arg.Metadata,
arg.CreatedAt,
)
var i Component
err := row.Scan(
&i.ID,
&i.ComponentTypeID,
&i.Name,
&i.Description,
&i.Unit,
&i.TotalQuantity,
&i.MinQuantity,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteComponent = `-- name: DeleteComponent :execrows
DELETE FROM components
WHERE id = $1
`
func (q *Queries) DeleteComponent(ctx context.Context, id int64) (int64, error) {
result, err := q.db.Exec(ctx, deleteComponent, id)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const getComponentByID = `-- name: GetComponentByID :one
SELECT id, component_type_id, name, description, unit, total_quantity, min_quantity, metadata, created_at, updated_at FROM components
WHERE id = $1
`
func (q *Queries) GetComponentByID(ctx context.Context, id int64) (Component, error) {
row := q.db.QueryRow(ctx, getComponentByID, id)
var i Component
err := row.Scan(
&i.ID,
&i.ComponentTypeID,
&i.Name,
&i.Description,
&i.Unit,
&i.TotalQuantity,
&i.MinQuantity,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listComponents = `-- name: ListComponents :many
SELECT id, component_type_id, name, description, unit, total_quantity, min_quantity, metadata, created_at, updated_at FROM components
ORDER BY created_at DESC
`
func (q *Queries) ListComponents(ctx context.Context) ([]Component, error) {
rows, err := q.db.Query(ctx, listComponents)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Component
for rows.Next() {
var i Component
if err := rows.Scan(
&i.ID,
&i.ComponentTypeID,
&i.Name,
&i.Description,
&i.Unit,
&i.TotalQuantity,
&i.MinQuantity,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateComponent = `-- name: UpdateComponent :one
UPDATE components
SET name = CASE WHEN $1 = '' THEN name ELSE $1 END,
component_type_id = coalesce($2, component_type_id),
description = coalesce($3, description),
unit = coalesce($4, unit),
min_quantity = coalesce($5, min_quantity),
metadata = coalesce($6, metadata),
updated_at = $7
WHERE id = $8
RETURNING id, component_type_id, name, description, unit, total_quantity, min_quantity, metadata, created_at, updated_at
`
type UpdateComponentParams struct {
Name interface{} `db:"name" json:"name"`
ComponentTypeID int64 `db:"component_type_id" json:"componentTypeId"`
Description pgtype.Text `db:"description" json:"description"`
Unit string `db:"unit" json:"unit"`
MinQuantity int32 `db:"min_quantity" json:"minQuantity"`
Metadata []byte `db:"metadata" json:"metadata"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
ID int64 `db:"id" json:"id"`
}
func (q *Queries) UpdateComponent(ctx context.Context, arg UpdateComponentParams) (Component, error) {
row := q.db.QueryRow(ctx, updateComponent,
arg.Name,
arg.ComponentTypeID,
arg.Description,
arg.Unit,
arg.MinQuantity,
arg.Metadata,
arg.UpdatedAt,
arg.ID,
)
var i Component
err := row.Scan(
&i.ID,
&i.ComponentTypeID,
&i.Name,
&i.Description,
&i.Unit,
&i.TotalQuantity,
&i.MinQuantity,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@@ -1,7 +1,7 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: conponent_type.sql
// source: component_type.sql
package db

View File

@@ -14,6 +14,7 @@ type Querier interface {
AssignRoleToUser(ctx context.Context, arg AssignRoleToUserParams) (UserRole, error)
CountUsersByRoleID(ctx context.Context, roleID uuid.UUID) (int64, error)
CreateCabinet(ctx context.Context, arg CreateCabinetParams) (Cabinet, error)
CreateComponent(ctx context.Context, arg CreateComponentParams) (Component, error)
CreateComponentType(ctx context.Context, arg CreateComponentTypeParams) (ComponentType, error)
CreateContainer(ctx context.Context, arg CreateContainerParams) (Container, error)
CreateRole(ctx context.Context, arg CreateRoleParams) (Role, error)
@@ -22,6 +23,7 @@ type Querier interface {
CreateUser(ctx context.Context, arg CreateUserParams) (uuid.UUID, error)
CreateWarehouse(ctx context.Context, arg CreateWarehouseParams) (Warehouse, error)
DeleteCabinet(ctx context.Context, id int64) (int64, error)
DeleteComponent(ctx context.Context, id int64) (int64, error)
DeleteComponentType(ctx context.Context, id int64) (int64, error)
DeleteContainer(ctx context.Context, id int64) (int64, error)
DeleteRole(ctx context.Context, id uuid.UUID) (int64, error)
@@ -29,6 +31,7 @@ type Querier interface {
DeleteShelve(ctx context.Context, id int64) (int64, error)
DeleteWarehouse(ctx context.Context, id int64) (int64, error)
GetCabinetByID(ctx context.Context, id int64) (Cabinet, error)
GetComponentByID(ctx context.Context, id int64) (Component, error)
GetComponentTypeByID(ctx context.Context, id int64) (ComponentType, error)
GetContainerByID(ctx context.Context, id int64) (Container, error)
GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error)
@@ -43,6 +46,7 @@ type Querier interface {
GetWarehouseByID(ctx context.Context, id int64) (Warehouse, error)
ListCabinets(ctx context.Context) ([]Cabinet, error)
ListComponentTypes(ctx context.Context) ([]ComponentType, error)
ListComponents(ctx context.Context) ([]Component, error)
ListContainers(ctx context.Context) ([]Container, error)
ListRoles(ctx context.Context) ([]Role, error)
ListRooms(ctx context.Context) ([]Room, error)
@@ -51,6 +55,7 @@ type Querier interface {
RemoveAllRolesFromUser(ctx context.Context, userID uuid.UUID) error
RemoveRoleFromUser(ctx context.Context, arg RemoveRoleFromUserParams) error
UpdateCabinet(ctx context.Context, arg UpdateCabinetParams) (Cabinet, error)
UpdateComponent(ctx context.Context, arg UpdateComponentParams) (Component, error)
UpdateComponentType(ctx context.Context, arg UpdateComponentTypeParams) (ComponentType, error)
UpdateContainer(ctx context.Context, arg UpdateContainerParams) (Container, error)
UpdateRole(ctx context.Context, arg UpdateRoleParams) (Role, error)