diff --git a/configs/constants/constants.go b/configs/constants/constants.go index 2852adc..8c56499 100644 --- a/configs/constants/constants.go +++ b/configs/constants/constants.go @@ -15,6 +15,7 @@ const ( API_GROUP_ROOM = "/rooms" API_GROUP_CABINET = "/cabinets" API_GROUP_SHELF = "/shelves" + API_GROUP_CONTAINER = "/containers" ) const ( diff --git a/db/queries/container.sql b/db/queries/container.sql new file mode 100644 index 0000000..5f7a03b --- /dev/null +++ b/db/queries/container.sql @@ -0,0 +1,35 @@ +-- name: GetContainerByID :one +SELECT * FROM containers +WHERE id = sqlc.arg(id); + +-- name: ListContainers :many +SELECT * FROM containers +ORDER BY created_at DESC; + +-- name: CreateContainer :one +INSERT INTO containers (shelf_id,name,container_type, description,max_capacity,metadata, created_at) +VALUES ( + sqlc.arg(shelf_id), + sqlc.arg(name), + sqlc.arg(container_type), + sqlc.arg(description), + sqlc.arg(max_capacity), + sqlc.arg(metadata), + sqlc.arg(created_at) +) +RETURNING *; + +-- name: UpdateContainer :one +UPDATE containers +SET name = CASE WHEN sqlc.arg(name) = '' THEN name ELSE sqlc.arg(name) END, + description = coalesce(sqlc.arg(description), description), + container_type = coalesce(sqlc.narg(container_type), container_type), + max_capacity = coalesce(sqlc.arg(max_capacity), max_capacity), + metadata = coalesce(sqlc.arg(metadata), metadata), + updated_at = sqlc.arg(updated_at) +WHERE id = sqlc.arg(id) +RETURNING *; + +-- name: DeleteContainer :execrows +DELETE FROM containers +WHERE id = sqlc.arg(id); diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 372d0bd..d31920f 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -378,6 +378,279 @@ const docTemplate = `{ } } }, + "/v1/containers": { + "get": { + "description": "Retrieve a list of all containers ordered by creation date", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "List all containers", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Container" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new container with the provided details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "Create a new container", + "parameters": [ + { + "description": "Container request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.CreateContainerRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.CreateContainerResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, + "/v1/containers/{id}": { + "get": { + "description": "Retrieve a single container using its unique identifier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "Get container by ID", + "parameters": [ + { + "type": "integer", + "description": "Container ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.Container" + } + } + } + ] + } + }, + "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 container by its ID. Only non-empty fields will be updated.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "Update container", + "parameters": [ + { + "type": "integer", + "description": "Container ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Container request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UpdateContainerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.UpdateContainerResponse" + } + } + } + ] + } + }, + "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 container by its unique identifier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "Delete container", + "parameters": [ + { + "type": "integer", + "description": "Container 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" + } + } + } + } + }, "/v1/rooms": { "get": { "description": "Retrieve a list of all rooms ordered by creation date", @@ -1216,6 +1489,41 @@ const docTemplate = `{ } } }, + "models.Container": { + "type": "object", + "properties": { + "containerType": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "maxCapacity": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "shelfId": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, "models.Room": { "type": "object", "properties": { @@ -1329,6 +1637,37 @@ const docTemplate = `{ } } }, + "requests.CreateContainerRequest": { + "type": "object", + "required": [ + "containerType", + "name", + "shelfId" + ], + "properties": { + "containerType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "maxCapacity": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "shelfId": { + "type": "integer" + } + } + }, "requests.CreateRoomRequest": { "type": "object", "required": [ @@ -1398,6 +1737,29 @@ const docTemplate = `{ } } }, + "requests.UpdateContainerRequest": { + "type": "object", + "properties": { + "containerType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "maxCapacity": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + } + } + }, "requests.UpdateRoomRequest": { "type": "object", "properties": { @@ -1486,6 +1848,14 @@ const docTemplate = `{ } } }, + "responses.CreateContainerResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, "responses.CreateRoomResponse": { "type": "object", "properties": { @@ -1527,6 +1897,35 @@ const docTemplate = `{ } } }, + "responses.UpdateContainerResponse": { + "type": "object", + "properties": { + "containerType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "maxCapacity": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "shelfId": { + "type": "integer" + } + } + }, "responses.UpdateRoomResponse": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 6a36769..db05b79 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -372,6 +372,279 @@ } } }, + "/v1/containers": { + "get": { + "description": "Retrieve a list of all containers ordered by creation date", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "List all containers", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Container" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new container with the provided details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "Create a new container", + "parameters": [ + { + "description": "Container request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.CreateContainerRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.CreateContainerResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.ErrorResponse" + } + } + } + } + }, + "/v1/containers/{id}": { + "get": { + "description": "Retrieve a single container using its unique identifier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "Get container by ID", + "parameters": [ + { + "type": "integer", + "description": "Container ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.Container" + } + } + } + ] + } + }, + "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 container by its ID. Only non-empty fields will be updated.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "Update container", + "parameters": [ + { + "type": "integer", + "description": "Container ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Container request body", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.UpdateContainerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.SuccessResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/responses.UpdateContainerResponse" + } + } + } + ] + } + }, + "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 container by its unique identifier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "container" + ], + "summary": "Delete container", + "parameters": [ + { + "type": "integer", + "description": "Container 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" + } + } + } + } + }, "/v1/rooms": { "get": { "description": "Retrieve a list of all rooms ordered by creation date", @@ -1210,6 +1483,41 @@ } } }, + "models.Container": { + "type": "object", + "properties": { + "containerType": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "maxCapacity": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "shelfId": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, "models.Room": { "type": "object", "properties": { @@ -1323,6 +1631,37 @@ } } }, + "requests.CreateContainerRequest": { + "type": "object", + "required": [ + "containerType", + "name", + "shelfId" + ], + "properties": { + "containerType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "maxCapacity": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "shelfId": { + "type": "integer" + } + } + }, "requests.CreateRoomRequest": { "type": "object", "required": [ @@ -1392,6 +1731,29 @@ } } }, + "requests.UpdateContainerRequest": { + "type": "object", + "properties": { + "containerType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "maxCapacity": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + } + } + }, "requests.UpdateRoomRequest": { "type": "object", "properties": { @@ -1480,6 +1842,14 @@ } } }, + "responses.CreateContainerResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, "responses.CreateRoomResponse": { "type": "object", "properties": { @@ -1521,6 +1891,35 @@ } } }, + "responses.UpdateContainerResponse": { + "type": "object", + "properties": { + "containerType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "maxCapacity": { + "type": "integer" + }, + "metadata": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "shelfId": { + "type": "integer" + } + } + }, "responses.UpdateRoomResponse": { "type": "object", "properties": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 66f71a2..640c653 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -15,6 +15,29 @@ definitions: updatedAt: type: string type: object + models.Container: + properties: + containerType: + type: string + createdAt: + type: string + description: + type: string + id: + type: integer + maxCapacity: + type: integer + metadata: + items: + type: integer + type: array + name: + type: string + shelfId: + type: integer + updatedAt: + type: string + type: object models.Room: properties: createdAt: @@ -90,6 +113,27 @@ definitions: - name - roomId type: object + requests.CreateContainerRequest: + properties: + containerType: + type: string + description: + type: string + maxCapacity: + type: integer + metadata: + items: + type: integer + type: array + name: + type: string + shelfId: + type: integer + required: + - containerType + - name + - shelfId + type: object requests.CreateRoomRequest: properties: description: @@ -136,6 +180,21 @@ definitions: name: type: string type: object + requests.UpdateContainerRequest: + properties: + containerType: + type: string + description: + type: string + maxCapacity: + type: integer + metadata: + items: + type: integer + type: array + name: + type: string + type: object requests.UpdateRoomRequest: properties: description: @@ -193,6 +252,11 @@ definitions: id: type: integer type: object + responses.CreateContainerResponse: + properties: + id: + type: integer + type: object responses.CreateRoomResponse: properties: id: @@ -219,6 +283,25 @@ definitions: roomId: type: integer type: object + responses.UpdateContainerResponse: + properties: + containerType: + type: string + description: + type: string + id: + type: integer + maxCapacity: + type: integer + metadata: + items: + type: integer + type: array + name: + type: string + shelfId: + type: integer + type: object responses.UpdateRoomResponse: properties: description: @@ -487,6 +570,176 @@ paths: summary: Update cabinet tags: - cabinet + /v1/containers: + get: + consumes: + - application/json + description: Retrieve a list of all containers ordered by creation date + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + items: + $ref: '#/definitions/models.Container' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.ErrorResponse' + summary: List all containers + tags: + - container + post: + consumes: + - application/json + description: Create a new container with the provided details + parameters: + - description: Container request body + in: body + name: body + required: true + schema: + $ref: '#/definitions/requests.CreateContainerRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/responses.CreateContainerResponse' + 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 container + tags: + - container + /v1/containers/{id}: + delete: + consumes: + - application/json + description: Delete a container by its unique identifier + parameters: + - description: Container 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 container + tags: + - container + get: + consumes: + - application/json + description: Retrieve a single container using its unique identifier + parameters: + - description: Container 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.Container' + 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 container by ID + tags: + - container + put: + consumes: + - application/json + description: Update an existing container by its ID. Only non-empty fields will + be updated. + parameters: + - description: Container ID + in: path + name: id + required: true + type: integer + - description: Container request body + in: body + name: body + required: true + schema: + $ref: '#/definitions/requests.UpdateContainerRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.SuccessResponse' + - properties: + data: + $ref: '#/definitions/responses.UpdateContainerResponse' + 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 container + tags: + - container /v1/rooms: get: consumes: diff --git a/internal/mapper/container_mapper.go b/internal/mapper/container_mapper.go new file mode 100644 index 0000000..755137e --- /dev/null +++ b/internal/mapper/container_mapper.go @@ -0,0 +1,61 @@ +package mapper + +import ( + "wm-backend/internal/models" + db "wm-backend/sqlc_gen" + + "github.com/jackc/pgx/v5/pgtype" +) + +func ToDomainContainer(r db.Container) *models.Container { + return &models.Container{ + ID: r.ID, + ShelfID: r.ShelfID, + Name: r.Name, + ContainerType: string(r.ContainerType), + Description: r.Description.String, + MaxCapacity: r.MaxCapacity.Int32, + Metadata: r.Metadata, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + } +} + +func ToModelContainer(r *models.Container) *db.CreateContainerParams { + return &db.CreateContainerParams{ + ShelfID: r.ShelfID, + Name: r.Name, + ContainerType: db.ContainerTypeEnum(r.ContainerType), + Description: pgtype.Text{ + String: r.Description, + Valid: r.Description != "", + }, + MaxCapacity: pgtype.Int4{ + Int32: r.MaxCapacity, + Valid: r.MaxCapacity != 0, + }, + Metadata: r.Metadata, + CreatedAt: r.CreatedAt, + } +} + +func ToUpdateModelContainer(r *models.Container) *db.UpdateContainerParams { + return &db.UpdateContainerParams{ + Name: r.Name, + ContainerType: db.NullContainerTypeEnum{ + ContainerTypeEnum: db.ContainerTypeEnum(r.ContainerType), + Valid: r.ContainerType != "", + }, + Description: pgtype.Text{ + String: r.Description, + Valid: r.Description != "", + }, + MaxCapacity: pgtype.Int4{ + Int32: r.MaxCapacity, + Valid: r.MaxCapacity != 0, + }, + Metadata: r.Metadata, + UpdatedAt: r.UpdatedAt, + ID: r.ID, + } +} diff --git a/internal/models/container_model.go b/internal/models/container_model.go new file mode 100644 index 0000000..afdc73f --- /dev/null +++ b/internal/models/container_model.go @@ -0,0 +1,18 @@ +package models + +import ( + "encoding/json" + "time" +) + +type Container struct { + ID int64 `json:"id"` + ShelfID int64 `json:"shelfId"` + Name string `json:"name"` + ContainerType string `json:"containerType"` + Description string `json:"description"` + MaxCapacity int32 `json:"maxCapacity"` + Metadata json.RawMessage `json:"metadata"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/internal/models/requests/container_request.go b/internal/models/requests/container_request.go new file mode 100644 index 0000000..b9b6e92 --- /dev/null +++ b/internal/models/requests/container_request.go @@ -0,0 +1,20 @@ +package requests + +import "encoding/json" + +type CreateContainerRequest struct { + ShelfID int64 `json:"shelfId" binding:"required"` + Name string `json:"name" binding:"required"` + ContainerType string `json:"containerType" binding:"required"` + Description string `json:"description"` + MaxCapacity int32 `json:"maxCapacity"` + Metadata json.RawMessage `json:"metadata"` +} + +type UpdateContainerRequest struct { + Name string `json:"name"` + ContainerType string `json:"containerType"` + Description string `json:"description"` + MaxCapacity int32 `json:"maxCapacity"` + Metadata json.RawMessage `json:"metadata"` +} diff --git a/internal/models/responses/container_response.go b/internal/models/responses/container_response.go new file mode 100644 index 0000000..18af4f1 --- /dev/null +++ b/internal/models/responses/container_response.go @@ -0,0 +1,17 @@ +package responses + +import "encoding/json" + +type CreateContainerResponse struct { + ID int64 `json:"id"` +} + +type UpdateContainerResponse struct { + ID int64 `json:"id"` + ShelfID int64 `json:"shelfId"` + Name string `json:"name"` + ContainerType string `json:"containerType"` + Description string `json:"description"` + MaxCapacity int32 `json:"maxCapacity"` + Metadata json.RawMessage `json:"metadata"` +} diff --git a/internal/repositories/container_repository.go b/internal/repositories/container_repository.go new file mode 100644 index 0000000..cc02e74 --- /dev/null +++ b/internal/repositories/container_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 CreateContainer(ctx context.Context, queries *db.Queries, body models.Container) (models.Container, error) { + result, err := queries.CreateContainer(ctx, *mapper.ToModelContainer(&body)) + if err != nil { + return models.Container{}, err + } + return *mapper.ToDomainContainer(result), nil +} + +func GetContainerByID(ctx context.Context, queries *db.Queries, id int64) (models.Container, error) { + result, err := queries.GetContainerByID(ctx, id) + if err != nil { + return models.Container{}, err + } + return *mapper.ToDomainContainer(result), nil +} + +func ListContainers(ctx context.Context, queries *db.Queries) ([]models.Container, error) { + results, err := queries.ListContainers(ctx) + if err != nil { + return nil, err + } + var items []models.Container + for _, r := range results { + items = append(items, *mapper.ToDomainContainer(r)) + } + return items, nil +} + +func UpdateContainer(ctx context.Context, queries *db.Queries, body models.Container) (models.Container, error) { + result, err := queries.UpdateContainer(ctx, *mapper.ToUpdateModelContainer(&body)) + if err != nil { + return models.Container{}, err + } + return *mapper.ToDomainContainer(result), nil +} + +func DeleteContainer(ctx context.Context, queries *db.Queries, id int64) (int64, error) { + rowsAffected, err := queries.DeleteContainer(ctx, id) + log.Info().Int64("id", id).Int64("rowsAffected", rowsAffected).Msg("Deleted container") + if err != nil { + return rowsAffected, err + } + return rowsAffected, nil +} diff --git a/internal/routers/router.go b/internal/routers/router.go index ee0e9e2..d2d91e6 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -64,6 +64,15 @@ func NewRouter() *gin.Engine { shelve.PUT("/:id", utils.AsyncHandler(services.ShelveUpdate)) shelve.DELETE("/:id", utils.AsyncHandler(services.ShelveDelete)) } + + container := v1.Group(constants.API_GROUP_CONTAINER) + { + container.GET("", utils.AsyncHandler(services.ContainerList)) + container.GET("/:id", utils.AsyncHandler(services.ContainerGetByID)) + container.POST("", utils.AsyncHandler(services.ContainerCreate)) + container.PUT("/:id", utils.AsyncHandler(services.ContainerUpdate)) + container.DELETE("/:id", utils.AsyncHandler(services.ContainerDelete)) + } } r.GET(constants.API_PATH_PING, services.PingHandler) diff --git a/internal/services/container_service.go b/internal/services/container_service.go new file mode 100644 index 0000000..f2a57ff --- /dev/null +++ b/internal/services/container_service.go @@ -0,0 +1,202 @@ +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" +) + +// ContainerCreate creates a new container. +// It validates the request body and creates the container in the database. +// +// @Summary Create a new container +// @Description Create a new container with the provided details +// @Tags container +// @Accept json +// @Produce json +// @Param body body requests.CreateContainerRequest true "Container request body" +// @Success 201 {object} response.SuccessResponse{data=responses.CreateContainerResponse} +// @Failure 400 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/containers [post] +func ContainerCreate(c *gin.Context) error { + requestBody := requests.CreateContainerRequest{} + if helper.IsShouldBindJSON(c, &requestBody) { + return nil + } + containerModel := &models.Container{ + ShelfID: requestBody.ShelfID, + Name: requestBody.Name, + ContainerType: requestBody.ContainerType, + Description: requestBody.Description, + MaxCapacity: requestBody.MaxCapacity, + Metadata: requestBody.Metadata, + CreatedAt: time.Now(), + } + container, err := repositories.CreateContainer(c.Request.Context(), global.Queries, *containerModel) + if err != nil { + log.Error().Err(err).Msg("Failed to create container") + response.InternalServerError(c, http.StatusInternalServerError, "Failed to create container") + return nil + } + response.Created(c, "Container created successfully", &responses.CreateContainerResponse{ + ID: container.ID, + }) + return nil +} + +// ContainerGetByID retrieves a single container by its ID. +// +// @Summary Get container by ID +// @Description Retrieve a single container using its unique identifier +// @Tags container +// @Accept json +// @Produce json +// @Param id path int true "Container ID" +// @Success 200 {object} response.SuccessResponse{data=models.Container} +// @Failure 400 {object} response.ErrorResponse +// @Failure 404 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/containers/{id} [get] +func ContainerGetByID(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 + } + container, err := repositories.GetContainerByID(c.Request.Context(), global.Queries, id) + if err != nil { + log.Error().Err(err).Msgf("Failed to get container by ID: %d", id) + response.NotFoundError(c, http.StatusNotFound, "Container not found") + return nil + } + response.Ok(c, "Success", container) + return nil +} + +// ContainerList retrieves all containers. +// +// @Summary List all containers +// @Description Retrieve a list of all containers ordered by creation date +// @Tags container +// @Accept json +// @Produce json +// @Success 200 {object} response.SuccessResponse{data=[]models.Container} +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/containers [get] +func ContainerList(c *gin.Context) error { + containers, err := repositories.ListContainers(c.Request.Context(), global.Queries) + if err != nil { + response.InternalServerError(c, http.StatusInternalServerError, "Failed to list containers") + return nil + } + response.Ok(c, "Success", containers) + return nil +} + +// ContainerUpdate updates an existing container by its ID. +// It validates the request body, fetches the existing record, +// merges non-empty fields from the request, and updates the container in the database. +// +// @Summary Update container +// @Description Update an existing container by its ID. Only non-empty fields will be updated. +// @Tags container +// @Accept json +// @Produce json +// @Param id path int true "Container ID" +// @Param body body requests.UpdateContainerRequest true "Container request body" +// @Success 200 {object} response.SuccessResponse{data=responses.UpdateContainerResponse} +// @Failure 400 {object} response.ErrorResponse +// @Failure 404 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/containers/{id} [put] +func ContainerUpdate(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.UpdateContainerRequest{} + if helper.IsShouldBindJSON(c, &requestBody) { + return nil + } + existing, err := repositories.GetContainerByID(c.Request.Context(), global.Queries, id) + if err != nil { + response.NotFoundError(c, http.StatusNotFound, "Container not found") + return nil + } + if requestBody.Name != "" { + existing.Name = requestBody.Name + } + if requestBody.ContainerType != "" { + existing.ContainerType = requestBody.ContainerType + } + if requestBody.Description != "" { + existing.Description = requestBody.Description + } + if requestBody.MaxCapacity != 0 { + existing.MaxCapacity = requestBody.MaxCapacity + } + if len(requestBody.Metadata) > 0 { + existing.Metadata = requestBody.Metadata + } + existing.UpdatedAt = time.Now() + container, err := repositories.UpdateContainer(c.Request.Context(), global.Queries, existing) + if err != nil { + log.Error().Err(err).Msgf("Failed to update container with ID: %d", id) + response.InternalServerError(c, http.StatusInternalServerError, "Failed to update container") + return nil + } + response.Ok(c, "Container updated successfully", &responses.UpdateContainerResponse{ + ID: container.ID, + ShelfID: container.ShelfID, + Name: container.Name, + ContainerType: container.ContainerType, + Description: container.Description, + MaxCapacity: container.MaxCapacity, + Metadata: container.Metadata, + }) + return nil +} + +// ContainerDelete deletes a container by its ID. +// +// @Summary Delete container +// @Description Delete a container by its unique identifier +// @Tags container +// @Accept json +// @Produce json +// @Param id path int true "Container ID" +// @Success 200 {object} response.SuccessResponse +// @Failure 400 {object} response.ErrorResponse +// @Failure 500 {object} response.ErrorResponse +// @Router /v1/containers/{id} [delete] +func ContainerDelete(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.DeleteContainer(c.Request.Context(), global.Queries, id) + if err != nil { + log.Error().Err(err).Msgf("Failed to delete container with ID: %d", id) + response.InternalServerError(c, http.StatusInternalServerError, "Failed to delete container") + return nil + } + if rowsAffected == 0 { + response.NotFoundError(c, http.StatusNotFound, "Container not found") + return nil + } + response.Ok(c, "Delete Success", nil) + return nil +} diff --git a/internal/services/shelve_service.go b/internal/services/shelve_service.go index c62e52a..6cf6946 100644 --- a/internal/services/shelve_service.go +++ b/internal/services/shelve_service.go @@ -43,7 +43,7 @@ func ShelveCreate(c *gin.Context) error { } shelve, err := repositories.CreateShelve(c.Request.Context(), global.Queries, *shelveModel) if err != nil { - log.Error().Err(err).Msg("Failed to create shelve") + log.Error().Any("shelve", shelve).Err(err).Msg("Failed to create shelve") response.InternalServerError(c, http.StatusInternalServerError, "Failed to create shelve") return nil } diff --git a/sqlc_gen/container.sql.go b/sqlc_gen/container.sql.go new file mode 100644 index 0000000..3a85dc9 --- /dev/null +++ b/sqlc_gen/container.sql.go @@ -0,0 +1,179 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: container.sql + +package db + +import ( + "context" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createContainer = `-- name: CreateContainer :one +INSERT INTO containers (shelf_id,name,container_type, description,max_capacity,metadata, created_at) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING id, shelf_id, name, container_type, description, max_capacity, metadata, created_at, updated_at +` + +type CreateContainerParams struct { + ShelfID int64 `db:"shelf_id" json:"shelfId"` + Name string `db:"name" json:"name"` + ContainerType ContainerTypeEnum `db:"container_type" json:"containerType"` + Description pgtype.Text `db:"description" json:"description"` + MaxCapacity pgtype.Int4 `db:"max_capacity" json:"maxCapacity"` + Metadata []byte `db:"metadata" json:"metadata"` + CreatedAt time.Time `db:"created_at" json:"createdAt"` +} + +func (q *Queries) CreateContainer(ctx context.Context, arg CreateContainerParams) (Container, error) { + row := q.db.QueryRow(ctx, createContainer, + arg.ShelfID, + arg.Name, + arg.ContainerType, + arg.Description, + arg.MaxCapacity, + arg.Metadata, + arg.CreatedAt, + ) + var i Container + err := row.Scan( + &i.ID, + &i.ShelfID, + &i.Name, + &i.ContainerType, + &i.Description, + &i.MaxCapacity, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteContainer = `-- name: DeleteContainer :execrows +DELETE FROM containers +WHERE id = $1 +` + +func (q *Queries) DeleteContainer(ctx context.Context, id int64) (int64, error) { + result, err := q.db.Exec(ctx, deleteContainer, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const getContainerByID = `-- name: GetContainerByID :one +SELECT id, shelf_id, name, container_type, description, max_capacity, metadata, created_at, updated_at FROM containers +WHERE id = $1 +` + +func (q *Queries) GetContainerByID(ctx context.Context, id int64) (Container, error) { + row := q.db.QueryRow(ctx, getContainerByID, id) + var i Container + err := row.Scan( + &i.ID, + &i.ShelfID, + &i.Name, + &i.ContainerType, + &i.Description, + &i.MaxCapacity, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listContainers = `-- name: ListContainers :many +SELECT id, shelf_id, name, container_type, description, max_capacity, metadata, created_at, updated_at FROM containers +ORDER BY created_at DESC +` + +func (q *Queries) ListContainers(ctx context.Context) ([]Container, error) { + rows, err := q.db.Query(ctx, listContainers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Container + for rows.Next() { + var i Container + if err := rows.Scan( + &i.ID, + &i.ShelfID, + &i.Name, + &i.ContainerType, + &i.Description, + &i.MaxCapacity, + &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 updateContainer = `-- name: UpdateContainer :one +UPDATE containers +SET name = CASE WHEN $1 = '' THEN name ELSE $1 END, + description = coalesce($2, description), + container_type = coalesce($3, container_type), + max_capacity = coalesce($4, max_capacity), + metadata = coalesce($5, metadata), + updated_at = $6 +WHERE id = $7 +RETURNING id, shelf_id, name, container_type, description, max_capacity, metadata, created_at, updated_at +` + +type UpdateContainerParams struct { + Name interface{} `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` + ContainerType NullContainerTypeEnum `db:"container_type" json:"containerType"` + MaxCapacity pgtype.Int4 `db:"max_capacity" json:"maxCapacity"` + Metadata []byte `db:"metadata" json:"metadata"` + UpdatedAt time.Time `db:"updated_at" json:"updatedAt"` + ID int64 `db:"id" json:"id"` +} + +func (q *Queries) UpdateContainer(ctx context.Context, arg UpdateContainerParams) (Container, error) { + row := q.db.QueryRow(ctx, updateContainer, + arg.Name, + arg.Description, + arg.ContainerType, + arg.MaxCapacity, + arg.Metadata, + arg.UpdatedAt, + arg.ID, + ) + var i Container + err := row.Scan( + &i.ID, + &i.ShelfID, + &i.Name, + &i.ContainerType, + &i.Description, + &i.MaxCapacity, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/sqlc_gen/querier.go b/sqlc_gen/querier.go index e77ea1f..71e3938 100644 --- a/sqlc_gen/querier.go +++ b/sqlc_gen/querier.go @@ -14,17 +14,20 @@ 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) + CreateContainer(ctx context.Context, arg CreateContainerParams) (Container, error) CreateRole(ctx context.Context, arg CreateRoleParams) (Role, error) CreateRoom(ctx context.Context, arg CreateRoomParams) (Room, error) CreateShelve(ctx context.Context, arg CreateShelveParams) (Shelf, error) 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) + DeleteContainer(ctx context.Context, id int64) (int64, error) DeleteRole(ctx context.Context, id uuid.UUID) (int64, error) DeleteRoom(ctx context.Context, id int64) (int64, error) DeleteShelve(ctx context.Context, id int64) (int64, error) DeleteWarehouse(ctx context.Context, id int64) (int64, error) GetCabinetByID(ctx context.Context, id int64) (Cabinet, error) + GetContainerByID(ctx context.Context, id int64) (Container, error) GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error) GetRoomByID(ctx context.Context, id int64) (Room, error) GetShelveByID(ctx context.Context, id int64) (Shelf, error) @@ -36,6 +39,7 @@ type Querier interface { GetUserRolesByUserID(ctx context.Context, userID uuid.UUID) ([]GetUserRolesByUserIDRow, error) GetWarehouseByID(ctx context.Context, id int64) (Warehouse, error) ListCabinets(ctx context.Context) ([]Cabinet, error) + ListContainers(ctx context.Context) ([]Container, error) ListRoles(ctx context.Context) ([]Role, error) ListRooms(ctx context.Context) ([]Room, error) ListShelves(ctx context.Context) ([]Shelf, error) @@ -43,6 +47,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) + UpdateContainer(ctx context.Context, arg UpdateContainerParams) (Container, error) UpdateRole(ctx context.Context, arg UpdateRoleParams) (Role, error) UpdateRoom(ctx context.Context, arg UpdateRoomParams) (Room, error) UpdateShelve(ctx context.Context, arg UpdateShelveParams) (Shelf, error)