diff --git a/configs/constants/constants.go b/configs/constants/constants.go index 2e25098..72659e4 100644 --- a/configs/constants/constants.go +++ b/configs/constants/constants.go @@ -17,6 +17,7 @@ const ( API_GROUP_SHELF = "/shelves" API_GROUP_CONTAINER = "/containers" API_GROUP_COMPONENT_TYPE = "/component-types" + API_GROUP_COMPONENT = "/components" ) const ( diff --git a/db/queries/component.sql b/db/queries/component.sql new file mode 100644 index 0000000..e3fc0c9 --- /dev/null +++ b/db/queries/component.sql @@ -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); diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index c896d56..96ff4be 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -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": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index fa9f000..29c8491 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -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": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index f98245b..57300b2 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -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: diff --git a/internal/mapper/component_mapper.go b/internal/mapper/component_mapper.go new file mode 100644 index 0000000..f575fa3 --- /dev/null +++ b/internal/mapper/component_mapper.go @@ -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, + } +} diff --git a/internal/models/component_model.go b/internal/models/component_model.go new file mode 100644 index 0000000..2c3d0b2 --- /dev/null +++ b/internal/models/component_model.go @@ -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"` +} diff --git a/internal/models/requests/component_request.go b/internal/models/requests/component_request.go new file mode 100644 index 0000000..ceff67d --- /dev/null +++ b/internal/models/requests/component_request.go @@ -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"` +} diff --git a/internal/models/responses/component_response.go b/internal/models/responses/component_response.go new file mode 100644 index 0000000..f6a26d1 --- /dev/null +++ b/internal/models/responses/component_response.go @@ -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"` +} diff --git a/internal/repositories/component_repository.go b/internal/repositories/component_repository.go new file mode 100644 index 0000000..2149ecf --- /dev/null +++ b/internal/repositories/component_repository.go @@ -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 +} diff --git a/internal/routers/router.go b/internal/routers/router.go index 1b9cad4..0fadc96 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -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) diff --git a/internal/services/component_service.go b/internal/services/component_service.go new file mode 100644 index 0000000..63c2d05 --- /dev/null +++ b/internal/services/component_service.go @@ -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 +} diff --git a/sqlc_gen/component.sql.go b/sqlc_gen/component.sql.go new file mode 100644 index 0000000..93a3cc9 --- /dev/null +++ b/sqlc_gen/component.sql.go @@ -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 +} diff --git a/sqlc_gen/conponent_type.sql.go b/sqlc_gen/component_type.sql.go similarity index 99% rename from sqlc_gen/conponent_type.sql.go rename to sqlc_gen/component_type.sql.go index 090b43f..dbdddb5 100644 --- a/sqlc_gen/conponent_type.sql.go +++ b/sqlc_gen/component_type.sql.go @@ -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 diff --git a/sqlc_gen/querier.go b/sqlc_gen/querier.go index 6f4d170..f7e1830 100644 --- a/sqlc_gen/querier.go +++ b/sqlc_gen/querier.go @@ -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)