diff --git a/backend/app/api/docs/docs.go b/backend/app/api/docs/docs.go index c2a9f17..4c3fe07 100644 --- a/backend/app/api/docs/docs.go +++ b/backend/app/api/docs/docs.go @@ -261,6 +261,170 @@ const docTemplate = `{ } } }, + "/v1/items": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Get All Items", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.Results" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.ItemOut" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Create a new item", + "parameters": [ + { + "description": "Item Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.ItemCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.ItemSummary" + } + } + } + } + }, + "/v1/items/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Gets a item and fields", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.ItemOut" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "updates a item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.ItemOut" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "deletes a item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "" + } + } + } + }, "/v1/labels": { "get": { "security": [ @@ -453,7 +617,7 @@ const docTemplate = `{ "items": { "type": "array", "items": { - "$ref": "#/definitions/types.LocationOut" + "$ref": "#/definitions/types.LocationCount" } } } @@ -1249,6 +1413,94 @@ const docTemplate = `{ } } }, + "types.ItemCreate": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "locationId": { + "description": "Edges", + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "types.ItemOut": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "$ref": "#/definitions/types.LabelSummary" + } + }, + "location": { + "description": "Edges", + "$ref": "#/definitions/types.LocationSummary" + }, + "manufacturer": { + "type": "string" + }, + "modelNumber": { + "type": "string" + }, + "name": { + "type": "string" + }, + "notes": { + "description": "Extras", + "type": "string" + }, + "purchaseFrom": { + "type": "string" + }, + "purchasePrice": { + "type": "number" + }, + "purchaseTime": { + "description": "Purchase", + "type": "string" + }, + "serialNumber": { + "description": "Identifications", + "type": "string" + }, + "soldNotes": { + "type": "string" + }, + "soldPrice": { + "type": "number" + }, + "soldTime": { + "description": "Sold", + "type": "string" + }, + "soldTo": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, "types.ItemSummary": { "type": "object", "properties": { @@ -1261,12 +1513,56 @@ const docTemplate = `{ "id": { "type": "string" }, - "locationId": { + "labels": { + "type": "array", + "items": { + "$ref": "#/definitions/types.LabelSummary" + } + }, + "location": { + "description": "Edges", + "$ref": "#/definitions/types.LocationSummary" + }, + "manufacturer": { + "type": "string" + }, + "modelNumber": { "type": "string" }, "name": { "type": "string" }, + "notes": { + "description": "Extras", + "type": "string" + }, + "purchaseFrom": { + "type": "string" + }, + "purchasePrice": { + "type": "number" + }, + "purchaseTime": { + "description": "Purchase", + "type": "string" + }, + "serialNumber": { + "description": "Identifications", + "type": "string" + }, + "soldNotes": { + "type": "string" + }, + "soldPrice": { + "type": "number" + }, + "soldTime": { + "description": "Sold", + "type": "string" + }, + "soldTo": { + "type": "string" + }, "updatedAt": { "type": "string" } @@ -1338,6 +1634,32 @@ const docTemplate = `{ } } }, + "types.LocationCount": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "itemCount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, "types.LocationCreate": { "type": "object", "properties": { diff --git a/backend/app/api/docs/swagger.json b/backend/app/api/docs/swagger.json index dbfe062..260bbeb 100644 --- a/backend/app/api/docs/swagger.json +++ b/backend/app/api/docs/swagger.json @@ -253,6 +253,170 @@ } } }, + "/v1/items": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Get All Items", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/server.Results" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.ItemOut" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Create a new item", + "parameters": [ + { + "description": "Item Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.ItemCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.ItemSummary" + } + } + } + } + }, + "/v1/items/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Gets a item and fields", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.ItemOut" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "updates a item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.ItemOut" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "deletes a item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "" + } + } + } + }, "/v1/labels": { "get": { "security": [ @@ -445,7 +609,7 @@ "items": { "type": "array", "items": { - "$ref": "#/definitions/types.LocationOut" + "$ref": "#/definitions/types.LocationCount" } } } @@ -1241,6 +1405,94 @@ } } }, + "types.ItemCreate": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "locationId": { + "description": "Edges", + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "types.ItemOut": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "$ref": "#/definitions/types.LabelSummary" + } + }, + "location": { + "description": "Edges", + "$ref": "#/definitions/types.LocationSummary" + }, + "manufacturer": { + "type": "string" + }, + "modelNumber": { + "type": "string" + }, + "name": { + "type": "string" + }, + "notes": { + "description": "Extras", + "type": "string" + }, + "purchaseFrom": { + "type": "string" + }, + "purchasePrice": { + "type": "number" + }, + "purchaseTime": { + "description": "Purchase", + "type": "string" + }, + "serialNumber": { + "description": "Identifications", + "type": "string" + }, + "soldNotes": { + "type": "string" + }, + "soldPrice": { + "type": "number" + }, + "soldTime": { + "description": "Sold", + "type": "string" + }, + "soldTo": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, "types.ItemSummary": { "type": "object", "properties": { @@ -1253,12 +1505,56 @@ "id": { "type": "string" }, - "locationId": { + "labels": { + "type": "array", + "items": { + "$ref": "#/definitions/types.LabelSummary" + } + }, + "location": { + "description": "Edges", + "$ref": "#/definitions/types.LocationSummary" + }, + "manufacturer": { + "type": "string" + }, + "modelNumber": { "type": "string" }, "name": { "type": "string" }, + "notes": { + "description": "Extras", + "type": "string" + }, + "purchaseFrom": { + "type": "string" + }, + "purchasePrice": { + "type": "number" + }, + "purchaseTime": { + "description": "Purchase", + "type": "string" + }, + "serialNumber": { + "description": "Identifications", + "type": "string" + }, + "soldNotes": { + "type": "string" + }, + "soldPrice": { + "type": "number" + }, + "soldTime": { + "description": "Sold", + "type": "string" + }, + "soldTo": { + "type": "string" + }, "updatedAt": { "type": "string" } @@ -1330,6 +1626,32 @@ } } }, + "types.LocationCount": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "itemCount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, "types.LocationCreate": { "type": "object", "properties": { diff --git a/backend/app/api/docs/swagger.yaml b/backend/app/api/docs/swagger.yaml index 71f2970..e26ca1e 100644 --- a/backend/app/api/docs/swagger.yaml +++ b/backend/app/api/docs/swagger.yaml @@ -338,6 +338,66 @@ definitions: type: string type: array type: object + types.ItemCreate: + properties: + description: + type: string + labelIds: + items: + type: string + type: array + locationId: + description: Edges + type: string + name: + type: string + type: object + types.ItemOut: + properties: + createdAt: + type: string + description: + type: string + id: + type: string + labels: + items: + $ref: '#/definitions/types.LabelSummary' + type: array + location: + $ref: '#/definitions/types.LocationSummary' + description: Edges + manufacturer: + type: string + modelNumber: + type: string + name: + type: string + notes: + description: Extras + type: string + purchaseFrom: + type: string + purchasePrice: + type: number + purchaseTime: + description: Purchase + type: string + serialNumber: + description: Identifications + type: string + soldNotes: + type: string + soldPrice: + type: number + soldTime: + description: Sold + type: string + soldTo: + type: string + updatedAt: + type: string + type: object types.ItemSummary: properties: createdAt: @@ -346,10 +406,41 @@ definitions: type: string id: type: string - locationId: + labels: + items: + $ref: '#/definitions/types.LabelSummary' + type: array + location: + $ref: '#/definitions/types.LocationSummary' + description: Edges + manufacturer: + type: string + modelNumber: type: string name: type: string + notes: + description: Extras + type: string + purchaseFrom: + type: string + purchasePrice: + type: number + purchaseTime: + description: Purchase + type: string + serialNumber: + description: Identifications + type: string + soldNotes: + type: string + soldPrice: + type: number + soldTime: + description: Sold + type: string + soldTo: + type: string updatedAt: type: string type: object @@ -396,6 +487,23 @@ definitions: updatedAt: type: string type: object + types.LocationCount: + properties: + createdAt: + type: string + description: + type: string + groupId: + type: string + id: + type: string + itemCount: + type: integer + name: + type: string + updatedAt: + type: string + type: object types.LocationCreate: properties: description: @@ -626,6 +734,103 @@ paths: summary: Update a User tags: - 'Admin: Users' + /v1/items: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/server.Results' + - properties: + items: + items: + $ref: '#/definitions/types.ItemOut' + type: array + type: object + security: + - Bearer: [] + summary: Get All Items + tags: + - Items + post: + parameters: + - description: Item Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/types.ItemCreate' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/types.ItemSummary' + security: + - Bearer: [] + summary: Create a new item + tags: + - Items + /v1/items/{id}: + delete: + parameters: + - description: Item ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: "" + security: + - Bearer: [] + summary: deletes a item + tags: + - Items + get: + parameters: + - description: Item ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/types.ItemOut' + security: + - Bearer: [] + summary: Gets a item and fields + tags: + - Items + put: + parameters: + - description: Item ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/types.ItemOut' + security: + - Bearer: [] + summary: updates a item + tags: + - Items /v1/labels: get: produces: @@ -736,7 +941,7 @@ paths: - properties: items: items: - $ref: '#/definitions/types.LocationOut' + $ref: '#/definitions/types.LocationCount' type: array type: object security: diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index c50b39a..41e1344 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -61,6 +61,12 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux { r.Get(v1Base("/labels/{id}"), v1Handlers.HandleLabelGet()) r.Put(v1Base("/labels/{id}"), v1Handlers.HandleLabelUpdate()) r.Delete(v1Base("/labels/{id}"), v1Handlers.HandleLabelDelete()) + + r.Get(v1Base("/items"), v1Handlers.HandleItemsGetAll()) + r.Post(v1Base("/items"), v1Handlers.HandleItemsCreate()) + r.Get(v1Base("/items/{id}"), v1Handlers.HandleItemGet()) + r.Put(v1Base("/items/{id}"), v1Handlers.HandleItemUpdate()) + r.Delete(v1Base("/items/{id}"), v1Handlers.HandleItemDelete()) }) r.Group(func(r chi.Router) { diff --git a/backend/app/api/v1/v1_ctrl_items.go b/backend/app/api/v1/v1_ctrl_items.go new file mode 100644 index 0000000..f8d237b --- /dev/null +++ b/backend/app/api/v1/v1_ctrl_items.go @@ -0,0 +1,141 @@ +package v1 + +import ( + "net/http" + + "github.com/hay-kot/content/backend/internal/services" + "github.com/hay-kot/content/backend/internal/types" + "github.com/hay-kot/content/backend/pkgs/server" +) + +// HandleItemsGetAll godoc +// @Summary Get All Items +// @Tags Items +// @Produce json +// @Success 200 {object} server.Results{items=[]types.ItemOut} +// @Router /v1/items [GET] +// @Security Bearer +func (ctrl *V1Controller) HandleItemsGetAll() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := services.UseUserCtx(r.Context()) + items, err := ctrl.svc.Items.GetAll(r.Context(), user.GroupID) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + server.Respond(w, http.StatusOK, server.Results{Items: items}) + } +} + +// HandleItemsCreate godoc +// @Summary Create a new item +// @Tags Items +// @Produce json +// @Param payload body types.ItemCreate true "Item Data" +// @Success 200 {object} types.ItemSummary +// @Router /v1/items [POST] +// @Security Bearer +func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + createData := types.ItemCreate{} + if err := server.Decode(r, &createData); err != nil { + ctrl.log.Error(err, nil) + server.RespondError(w, http.StatusInternalServerError, err) + return + } + + user := services.UseUserCtx(r.Context()) + item, err := ctrl.svc.Items.Create(r.Context(), user.GroupID, createData) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + + server.Respond(w, http.StatusCreated, item) + + } +} + +// HandleItemDelete godocs +// @Summary deletes a item +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Success 204 +// @Router /v1/items/{id} [DELETE] +// @Security Bearer +func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + uid, user, err := ctrl.partialParseIdAndUser(w, r) + if err != nil { + return + } + + err = ctrl.svc.Items.Delete(r.Context(), user.GroupID, uid) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + server.Respond(w, http.StatusNoContent, nil) + } +} + +// HandleItemGet godocs +// @Summary Gets a item and fields +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Success 200 {object} types.ItemOut +// @Router /v1/items/{id} [GET] +// @Security Bearer +func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + uid, user, err := ctrl.partialParseIdAndUser(w, r) + if err != nil { + return + } + + items, err := ctrl.svc.Items.GetOne(r.Context(), user.GroupID, uid) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + server.Respond(w, http.StatusOK, items) + } +} + +// HandleItemUpdate godocs +// @Summary updates a item +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Success 200 {object} types.ItemOut +// @Router /v1/items/{id} [PUT] +// @Security Bearer +func (ctrl *V1Controller) HandleItemUpdate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body := types.ItemUpdate{} + if err := server.Decode(r, &body); err != nil { + ctrl.log.Error(err, nil) + server.RespondError(w, http.StatusInternalServerError, err) + return + } + uid, user, err := ctrl.partialParseIdAndUser(w, r) + if err != nil { + return + } + + body.ID = uid + result, err := ctrl.svc.Items.Update(r.Context(), user.GroupID, body) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + server.Respond(w, http.StatusOK, result) + } +} diff --git a/backend/app/api/v1/v1_ctrl_locations.go b/backend/app/api/v1/v1_ctrl_locations.go index 14b3775..f6d4df7 100644 --- a/backend/app/api/v1/v1_ctrl_locations.go +++ b/backend/app/api/v1/v1_ctrl_locations.go @@ -12,7 +12,7 @@ import ( // @Summary Get All Locations // @Tags Locations // @Produce json -// @Success 200 {object} server.Results{items=[]types.LocationOut} +// @Success 200 {object} server.Results{items=[]types.LocationCount} // @Router /v1/locations [GET] // @Security Bearer func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc { diff --git a/backend/ent/external.go b/backend/ent/external.go new file mode 100644 index 0000000..d094da8 --- /dev/null +++ b/backend/ent/external.go @@ -0,0 +1,13 @@ +package ent + +import ( + "database/sql" + + entsql "entgo.io/ent/dialect/sql" +) + +// Sql exposes the underlying database connection in the ent client +// so that we can use it to perform custom queries. +func (c *Client) Sql() *sql.DB { + return c.driver.(*entsql.Driver).DB() +} diff --git a/backend/internal/repo/repo_items.go b/backend/internal/repo/repo_items.go new file mode 100644 index 0000000..877f5b9 --- /dev/null +++ b/backend/internal/repo/repo_items.go @@ -0,0 +1,51 @@ +package repo + +import ( + "context" + + "github.com/google/uuid" + "github.com/hay-kot/content/backend/ent" + "github.com/hay-kot/content/backend/ent/group" + "github.com/hay-kot/content/backend/ent/item" + "github.com/hay-kot/content/backend/internal/types" +) + +type ItemsRepository struct { + db *ent.Client +} + +func (e *ItemsRepository) GetOne(ctx context.Context, id uuid.UUID) (*ent.Item, error) { + return e.db.Item.Query(). + Where(item.ID(id)). + WithFields(). + WithLabel(). + WithLocation(). + WithGroup(). + Only(ctx) +} + +func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]*ent.Item, error) { + return e.db.Item.Query(). + Where(item.HasGroupWith(group.ID(gid))). + WithLabel(). + WithLocation(). + All(ctx) +} + +func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data types.ItemCreate) (*ent.Item, error) { + return e.db.Item.Create(). + SetName(data.Name). + SetDescription(data.Description). + SetGroupID(gid). + AddLabelIDs(data.LabelIDs...). + SetLocationID(data.LocationID). + Save(ctx) +} + +func (e *ItemsRepository) Delete(ctx context.Context, gid uuid.UUID, id uuid.UUID) error { + panic("implement me") +} + +func (e *ItemsRepository) Update(ctx context.Context, gid uuid.UUID, data types.ItemUpdate) (*ent.Item, error) { + panic("implement me") +} diff --git a/backend/internal/repo/repo_locations.go b/backend/internal/repo/repo_locations.go index 3844b08..88a2724 100644 --- a/backend/internal/repo/repo_locations.go +++ b/backend/internal/repo/repo_locations.go @@ -5,7 +5,6 @@ import ( "github.com/google/uuid" "github.com/hay-kot/content/backend/ent" - "github.com/hay-kot/content/backend/ent/group" "github.com/hay-kot/content/backend/ent/location" "github.com/hay-kot/content/backend/internal/types" ) @@ -14,21 +13,64 @@ type EntLocationRepository struct { db *ent.Client } +type LocationWithCount struct { + *ent.Location + ItemCount int `json:"itemCount"` +} + +// GetALlWithCount returns all locations with item count field populated +func (r *EntLocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]LocationWithCount, error) { + query := ` + SELECT + id, + name, + description, + created_at, + updated_at, + ( + SELECT + COUNT(*) + FROM + items + WHERE + items.location_items = locations.id + ) as item_count + FROM + locations + WHERE + locations.group_locations = ? + ` + + rows, err := r.db.Sql().QueryContext(ctx, query, groupId) + if err != nil { + return nil, err + } + + list := []LocationWithCount{} + for rows.Next() { + var loc ent.Location + var ct LocationWithCount + err := rows.Scan(&loc.ID, &loc.Name, &loc.Description, &loc.CreatedAt, &loc.UpdatedAt, &ct.ItemCount) + if err != nil { + return nil, err + } + ct.Location = &loc + list = append(list, ct) + } + + return list, err +} + func (r *EntLocationRepository) Get(ctx context.Context, ID uuid.UUID) (*ent.Location, error) { return r.db.Location.Query(). Where(location.ID(ID)). WithGroup(). - WithItems(). + WithItems(func(iq *ent.ItemQuery) { + iq.WithLabel() + }). Only(ctx) } -func (r *EntLocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]*ent.Location, error) { - return r.db.Location.Query(). - Where(location.HasGroupWith(group.ID(groupId))). - WithGroup(). - All(ctx) -} - func (r *EntLocationRepository) Create(ctx context.Context, groupdId uuid.UUID, data types.LocationCreate) (*ent.Location, error) { location, err := r.db.Location.Create(). SetName(data.Name). diff --git a/backend/internal/repo/repo_locations_test.go b/backend/internal/repo/repo_locations_test.go index 683319c..1aefdea 100644 --- a/backend/internal/repo/repo_locations_test.go +++ b/backend/internal/repo/repo_locations_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hay-kot/content/backend/ent" "github.com/hay-kot/content/backend/internal/types" "github.com/hay-kot/content/backend/pkgs/faker" "github.com/stretchr/testify/assert" @@ -31,26 +30,30 @@ func Test_Locations_Get(t *testing.T) { testRepos.Locations.Delete(context.Background(), loc.ID) } -func Test_Locations_GetAll(t *testing.T) { - created := make([]*ent.Location, 6) +func Test_LocationsGetAllWithCount(t *testing.T) { + ctx := context.Background() + result, err := testRepos.Locations.Create(ctx, testGroup.ID, types.LocationCreate{ + Name: fk.RandomString(10), + Description: fk.RandomString(100), + }) - for i := 0; i < 6; i++ { - result, err := testRepos.Locations.Create(context.Background(), testGroup.ID, types.LocationCreate{ - Name: fk.RandomString(10), - Description: fk.RandomString(100), - }) + testRepos.Items.Create(ctx, testGroup.ID, types.ItemCreate{ + Name: fk.RandomString(10), + Description: fk.RandomString(100), + LocationID: result.ID, + }) - assert.NoError(t, err) - created[i] = result - } - - locations, err := testRepos.Locations.GetAll(context.Background(), testGroup.ID) assert.NoError(t, err) - assert.Equal(t, 6, len(locations)) - for _, loc := range created { - testRepos.Locations.Delete(context.Background(), loc.ID) + results, err := testRepos.Locations.GetAll(context.Background(), testGroup.ID) + assert.NoError(t, err) + + for _, loc := range results { + if loc.ID == result.ID { + assert.Equal(t, 1, loc.ItemCount) + } } + } func Test_Locations_Create(t *testing.T) { diff --git a/backend/internal/repo/repos_all.go b/backend/internal/repo/repos_all.go index 9fe144e..906d83c 100644 --- a/backend/internal/repo/repos_all.go +++ b/backend/internal/repo/repos_all.go @@ -9,6 +9,7 @@ type AllRepos struct { Groups *EntGroupRepository Locations *EntLocationRepository Labels *EntLabelRepository + Items *ItemsRepository } func EntAllRepos(db *ent.Client) *AllRepos { @@ -18,5 +19,6 @@ func EntAllRepos(db *ent.Client) *AllRepos { Groups: &EntGroupRepository{db}, Locations: &EntLocationRepository{db}, Labels: &EntLabelRepository{db}, + Items: &ItemsRepository{db}, } } diff --git a/backend/internal/services/all.go b/backend/internal/services/all.go index 4d26ae5..8978983 100644 --- a/backend/internal/services/all.go +++ b/backend/internal/services/all.go @@ -7,6 +7,7 @@ type AllServices struct { Admin *AdminService Location *LocationService Labels *LabelService + Items *ItemService } func NewServices(repos *repo.AllRepos) *AllServices { @@ -15,5 +16,6 @@ func NewServices(repos *repo.AllRepos) *AllServices { Admin: &AdminService{repos}, Location: &LocationService{repos}, Labels: &LabelService{repos}, + Items: &ItemService{repos}, } } diff --git a/backend/internal/services/mappers/items.go b/backend/internal/services/mappers/items.go index 475bb30..11ec73b 100644 --- a/backend/internal/services/mappers/items.go +++ b/backend/internal/services/mappers/items.go @@ -6,12 +6,58 @@ import ( ) func ToItemSummary(item *ent.Item) *types.ItemSummary { + var location *types.LocationSummary + if item.Edges.Location != nil { + location = ToLocationSummary(item.Edges.Location) + } + + var labels []*types.LabelSummary + if item.Edges.Label != nil { + labels = MapEach(item.Edges.Label, ToLabelSummary) + } + return &types.ItemSummary{ ID: item.ID, - LocationID: item.Edges.Location.ID, Name: item.Name, Description: item.Description, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, + + // Edges + Location: location, + Labels: labels, + + // Identification + SerialNumber: item.SerialNumber, + ModelNumber: item.ModelNumber, + Manufacturer: item.Manufacturer, + + // Purchase + PurchaseTime: item.PurchaseTime, + PurchaseFrom: item.PurchaseFrom, + PurchasePrice: item.PurchasePrice, + + // Sold + SoldTime: item.SoldTime, + SoldTo: item.SoldTo, + SoldPrice: item.SoldPrice, + SoldNotes: item.SoldNotes, + + // Extras + Notes: item.Notes, } } + +func ToItemSummaryErr(item *ent.Item, err error) (*types.ItemSummary, error) { + return ToItemSummary(item), err +} + +func ToItemOut(item *ent.Item) *types.ItemOut { + return &types.ItemOut{ + ItemSummary: *ToItemSummary(item), + } +} + +func ToItemOutErr(item *ent.Item, err error) (*types.ItemOut, error) { + return ToItemOut(item), err +} diff --git a/backend/internal/services/mappers/labels.go b/backend/internal/services/mappers/labels.go index a04e862..c5b886b 100644 --- a/backend/internal/services/mappers/labels.go +++ b/backend/internal/services/mappers/labels.go @@ -8,7 +8,6 @@ import ( func ToLabelSummary(label *ent.Label) *types.LabelSummary { return &types.LabelSummary{ ID: label.ID, - GroupID: label.Edges.Group.ID, Name: label.Name, Description: label.Description, CreatedAt: label.CreatedAt, diff --git a/backend/internal/services/mappers/locations.go b/backend/internal/services/mappers/locations.go index f8cb2a2..77f0eff 100644 --- a/backend/internal/services/mappers/locations.go +++ b/backend/internal/services/mappers/locations.go @@ -2,13 +2,30 @@ package mappers import ( "github.com/hay-kot/content/backend/ent" + "github.com/hay-kot/content/backend/internal/repo" "github.com/hay-kot/content/backend/internal/types" ) +func ToLocationCount(location *repo.LocationWithCount) *types.LocationCount { + return &types.LocationCount{ + LocationSummary: types.LocationSummary{ + ID: location.ID, + Name: location.Name, + Description: location.Description, + CreatedAt: location.CreatedAt, + UpdatedAt: location.UpdatedAt, + }, + ItemCount: location.ItemCount, + } +} + +func ToLocationCountErr(location *repo.LocationWithCount, err error) (*types.LocationCount, error) { + return ToLocationCount(location), err +} + func ToLocationSummary(location *ent.Location) *types.LocationSummary { return &types.LocationSummary{ ID: location.ID, - GroupID: location.Edges.Group.ID, Name: location.Name, Description: location.Description, CreatedAt: location.CreatedAt, @@ -22,8 +39,14 @@ func ToLocationSummaryErr(location *ent.Location, err error) (*types.LocationSum func ToLocationOut(location *ent.Location) *types.LocationOut { return &types.LocationOut{ - LocationSummary: *ToLocationSummary(location), - Items: MapEach(location.Edges.Items, ToItemSummary), + LocationSummary: types.LocationSummary{ + ID: location.ID, + Name: location.Name, + Description: location.Description, + CreatedAt: location.CreatedAt, + UpdatedAt: location.UpdatedAt, + }, + Items: MapEach(location.Edges.Items, ToItemSummary), } } diff --git a/backend/internal/services/service_items.go b/backend/internal/services/service_items.go new file mode 100644 index 0000000..3ec277d --- /dev/null +++ b/backend/internal/services/service_items.go @@ -0,0 +1,45 @@ +package services + +import ( + "context" + + "github.com/google/uuid" + "github.com/hay-kot/content/backend/internal/repo" + "github.com/hay-kot/content/backend/internal/services/mappers" + "github.com/hay-kot/content/backend/internal/types" +) + +type ItemService struct { + repo *repo.AllRepos +} + +func (svc *ItemService) GetOne(ctx context.Context, gid uuid.UUID, id uuid.UUID) (*types.ItemOut, error) { + panic("implement me") +} +func (svc *ItemService) GetAll(ctx context.Context, gid uuid.UUID) ([]*types.ItemSummary, error) { + items, err := svc.repo.Items.GetAll(ctx, gid) + if err != nil { + return nil, err + } + + itemsOut := make([]*types.ItemSummary, len(items)) + for i, item := range items { + itemsOut[i] = mappers.ToItemSummary(item) + } + + return itemsOut, nil +} +func (svc *ItemService) Create(ctx context.Context, gid uuid.UUID, data types.ItemCreate) (*types.ItemOut, error) { + item, err := svc.repo.Items.Create(ctx, gid, data) + if err != nil { + return nil, err + } + + return mappers.ToItemOut(item), nil +} +func (svc *ItemService) Delete(ctx context.Context, gid uuid.UUID, id uuid.UUID) error { + panic("implement me") +} +func (svc *ItemService) Update(ctx context.Context, gid uuid.UUID, data types.ItemUpdate) (*types.ItemOut, error) { + panic("implement me") +} diff --git a/backend/internal/services/service_locations.go b/backend/internal/services/service_locations.go index 6394e94..30db743 100644 --- a/backend/internal/services/service_locations.go +++ b/backend/internal/services/service_locations.go @@ -32,23 +32,23 @@ func (svc *LocationService) GetOne(ctx context.Context, groupId uuid.UUID, id uu return mappers.ToLocationOut(location), nil } -func (svc *LocationService) GetAll(ctx context.Context, groupId uuid.UUID) ([]*types.LocationSummary, error) { +func (svc *LocationService) GetAll(ctx context.Context, groupId uuid.UUID) ([]*types.LocationCount, error) { locations, err := svc.repos.Locations.GetAll(ctx, groupId) if err != nil { return nil, err } - locationsOut := make([]*types.LocationSummary, len(locations)) + locationsOut := make([]*types.LocationCount, len(locations)) for i, location := range locations { - locationsOut[i] = mappers.ToLocationSummary(location) + locationsOut[i] = mappers.ToLocationCount(&location) } return locationsOut, nil } -func (svc *LocationService) Create(ctx context.Context, groupId uuid.UUID, data types.LocationCreate) (*types.LocationSummary, error) { +func (svc *LocationService) Create(ctx context.Context, groupId uuid.UUID, data types.LocationCreate) (*types.LocationOut, error) { location, err := svc.repos.Locations.Create(ctx, groupId, data) - return mappers.ToLocationSummaryErr(location, err) + return mappers.ToLocationOutErr(location, err) } func (svc *LocationService) Delete(ctx context.Context, groupId uuid.UUID, id uuid.UUID) error { diff --git a/backend/internal/types/item_types.go b/backend/internal/types/item_types.go new file mode 100644 index 0000000..057faba --- /dev/null +++ b/backend/internal/types/item_types.go @@ -0,0 +1,83 @@ +package types + +import ( + "time" + + "github.com/google/uuid" +) + +type ItemCreate struct { + Name string `json:"name"` + Description string `json:"description"` + + // Edges + LocationID uuid.UUID `json:"locationId"` + LabelIDs []uuid.UUID `json:"labelIds"` +} + +type ItemUpdate struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + + // Edges + LocationID uuid.UUID `json:"locationId"` + LabelIDs []uuid.UUID `json:"labelIds"` + + // Identifications + SerialNumber string `json:"serialNumber"` + ModelNumber string `json:"modelNumber"` + Manufacturer string `json:"manufacturer"` + + // Purchase + PurchaseTime time.Time `json:"purchaseTime"` + PurchaseFrom string `json:"purchaseFrom"` + PurchasePrice float64 `json:"purchasePrice"` + + // Sold + SoldTime time.Time `json:"soldTime"` + SoldTo string `json:"soldTo"` + SoldPrice float64 `json:"soldPrice"` + SoldNotes string `json:"soldNotes"` + + // Extras + Notes string `json:"notes"` + // Fields []*FieldSummary `json:"fields"` +} + +type ItemSummary struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + + // Edges + Location *LocationSummary `json:"location"` + Labels []*LabelSummary `json:"labels"` + + // Identifications + SerialNumber string `json:"serialNumber"` + ModelNumber string `json:"modelNumber"` + Manufacturer string `json:"manufacturer"` + + // Purchase + PurchaseTime time.Time `json:"purchaseTime"` + PurchaseFrom string `json:"purchaseFrom"` + PurchasePrice float64 `json:"purchasePrice"` + + // Sold + SoldTime time.Time `json:"soldTime"` + SoldTo string `json:"soldTo"` + SoldPrice float64 `json:"soldPrice"` + SoldNotes string `json:"soldNotes"` + + // Extras + Notes string `json:"notes"` +} + +type ItemOut struct { + ItemSummary + // Future + // Fields []*FieldSummary `json:"fields"` +} diff --git a/backend/internal/types/location_types.go b/backend/internal/types/location_types.go index f60adf4..0916c2d 100644 --- a/backend/internal/types/location_types.go +++ b/backend/internal/types/location_types.go @@ -19,20 +19,15 @@ type LocationUpdate struct { type LocationSummary struct { ID uuid.UUID `json:"id"` - GroupID uuid.UUID `json:"groupId"` Name string `json:"name"` Description string `json:"description"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } -type ItemSummary struct { - ID uuid.UUID `json:"id"` - LocationID uuid.UUID `json:"locationId"` - Name string `json:"name"` - Description string `json:"description"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` +type LocationCount struct { + LocationSummary + ItemCount int `json:"itemCount"` } type LocationOut struct {