From 48036eabce4a39a9ba5d7f2e8f6055b6a15883e0 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Wed, 14 Sep 2022 20:05:13 -0800 Subject: [PATCH] implement attachment post route (WIP) --- .gitignore | 1 + backend/app/api/docs/docs.go | 57 ++++++++++++++++ backend/app/api/docs/swagger.json | 57 ++++++++++++++++ backend/app/api/docs/swagger.yaml | 37 ++++++++++ backend/app/api/routes.go | 2 + backend/app/api/v1/v1_ctrl_items.go | 67 ++++++++++++++++++- backend/internal/services/mappers/items.go | 1 + backend/internal/services/service_items.go | 4 +- .../internal/services/service_items_test.go | 2 +- backend/internal/types/item_types.go | 1 + backend/internal/types/label_types.go | 1 - frontend/lib/api/types/data-contracts.ts | 1 + 12 files changed, 225 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 0aeaf71..b81b742 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Project Specific +backend/homebox-data/* config.yml homebox.db .idea diff --git a/backend/app/api/docs/docs.go b/backend/app/api/docs/docs.go index 90e77ce..c827abb 100644 --- a/backend/app/api/docs/docs.go +++ b/backend/app/api/docs/docs.go @@ -224,6 +224,60 @@ const docTemplate = `{ } } }, + "/v1/items/{id}/attachment": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "imports items into the database", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "File attachment", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Type of file", + "name": "type", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "name of the file including extension", + "name": "name", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.ItemOut" + } + } + } + } + }, "/v1/labels": { "get": { "security": [ @@ -881,6 +935,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "type": { + "type": "string" + }, "updatedAt": { "type": "string" } diff --git a/backend/app/api/docs/swagger.json b/backend/app/api/docs/swagger.json index 3d56184..78239a0 100644 --- a/backend/app/api/docs/swagger.json +++ b/backend/app/api/docs/swagger.json @@ -216,6 +216,60 @@ } } }, + "/v1/items/{id}/attachment": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "imports items into the database", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "File attachment", + "name": "file", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "Type of file", + "name": "type", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "name of the file including extension", + "name": "name", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/types.ItemOut" + } + } + } + } + }, "/v1/labels": { "get": { "security": [ @@ -873,6 +927,9 @@ "id": { "type": "string" }, + "type": { + "type": "string" + }, "updatedAt": { "type": "string" } diff --git a/backend/app/api/docs/swagger.yaml b/backend/app/api/docs/swagger.yaml index 10cb358..92506b7 100644 --- a/backend/app/api/docs/swagger.yaml +++ b/backend/app/api/docs/swagger.yaml @@ -55,6 +55,8 @@ definitions: $ref: '#/definitions/types.DocumentOut' id: type: string + type: + type: string updatedAt: type: string type: object @@ -503,6 +505,41 @@ paths: summary: updates a item tags: - Items + /v1/items/{id}/attachment: + post: + parameters: + - description: Item ID + in: path + name: id + required: true + type: string + - description: File attachment + in: formData + name: file + required: true + type: file + - description: Type of file + in: formData + name: type + required: true + type: string + - description: name of the file including extension + in: formData + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/types.ItemOut' + security: + - Bearer: [] + summary: imports items into the database + tags: + - Items /v1/items/import: post: parameters: diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index b7cd126..863869a 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -81,6 +81,8 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux { r.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet()) r.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate()) r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete()) + + r.Post(v1Base("/items/{id}/attachment"), v1Ctrl.HandleItemAttachmentCreate()) }) } diff --git a/backend/app/api/v1/v1_ctrl_items.go b/backend/app/api/v1/v1_ctrl_items.go index 61e5d3e..6bf25b7 100644 --- a/backend/app/api/v1/v1_ctrl_items.go +++ b/backend/app/api/v1/v1_ctrl_items.go @@ -2,8 +2,10 @@ package v1 import ( "encoding/csv" + "errors" "net/http" + "github.com/hay-kot/homebox/backend/ent/attachment" "github.com/hay-kot/homebox/backend/internal/services" "github.com/hay-kot/homebox/backend/internal/types" "github.com/hay-kot/homebox/backend/pkgs/server" @@ -89,8 +91,8 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc { // @Summary Gets a item and fields // @Tags Items // @Produce json -// @Param id path string true "Item ID" -// @Success 200 {object} types.ItemOut +// @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 { @@ -189,3 +191,64 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc { server.Respond(w, http.StatusNoContent, nil) } } + +// HandleItemsImport godocs +// @Summary imports items into the database +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Param file formData file true "File attachment" +// @Param type formData string true "Type of file" +// @Param name formData string true "name of the file including extension" +// @Success 200 {object} types.ItemOut +// @Router /v1/items/{id}/attachment [Post] +// @Security Bearer +func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Max upload size of 10 MB - TODO: Set via config + err := r.ParseMultipartForm(10 << 20) + if err != nil { + log.Err(err).Msg("failed to parse multipart form") + server.RespondServerError(w) + return + } + file, _, err := r.FormFile("file") + if err != nil { + log.Err(err).Msg("failed to get file from form") + server.RespondServerError(w) + return + } + attachmentName := r.FormValue("name") + if attachmentName == "" { + log.Err(err).Msg("failed to get name from form") + server.RespondError(w, http.StatusBadRequest, errors.New("name is required")) + } + + attachmentType := r.FormValue("type") + if attachmentType == "" { + attachmentName = "attachment" + } + + uid, user, err := ctrl.partialParseIdAndUser(w, r) + if err != nil { + return + } + + item, err := ctrl.svc.Items.AddAttachment( + r.Context(), + user.GroupID, + uid, + attachmentName, + attachment.Type(attachmentType), + file, + ) + + if err != nil { + log.Err(err).Msg("failed to add attachment") + server.RespondServerError(w) + return + } + + server.Respond(w, http.StatusOK, item) + } +} diff --git a/backend/internal/services/mappers/items.go b/backend/internal/services/mappers/items.go index 2eb011d..6b9df8d 100644 --- a/backend/internal/services/mappers/items.go +++ b/backend/internal/services/mappers/items.go @@ -10,6 +10,7 @@ func ToItemAttachment(attachment *ent.Attachment) *types.ItemAttachment { ID: attachment.ID, CreatedAt: attachment.CreatedAt, UpdatedAt: attachment.UpdatedAt, + Type: attachment.Type.String(), Document: types.DocumentOut{ ID: attachment.Edges.Document.ID, Title: attachment.Edges.Document.Title, diff --git a/backend/internal/services/service_items.go b/backend/internal/services/service_items.go index b3e0615..3aa9253 100644 --- a/backend/internal/services/service_items.go +++ b/backend/internal/services/service_items.go @@ -101,7 +101,7 @@ func (svc *ItemService) attachmentPath(gid, itemId uuid.UUID, filename string) s // AddAttachment adds an attachment to an item by creating an entry in the Documents table and linking it to the Attachment // Table and Items table. The file provided via the reader is stored on the file system based on the provided // relative path during construction of the service. -func (svc *ItemService) AddAttachment(ctx context.Context, gid, itemId uuid.UUID, filename string, file io.Reader) (*types.ItemOut, error) { +func (svc *ItemService) AddAttachment(ctx context.Context, gid, itemId uuid.UUID, filename string, attachmentType attachment.Type, file io.Reader) (*types.ItemOut, error) { // Get the Item item, err := svc.repo.Items.GetOne(ctx, itemId) if err != nil { @@ -122,7 +122,7 @@ func (svc *ItemService) AddAttachment(ctx context.Context, gid, itemId uuid.UUID } // Create the attachment - _, err = svc.repo.Attachments.Create(ctx, itemId, doc.ID, attachment.TypeAttachment) + _, err = svc.repo.Attachments.Create(ctx, itemId, doc.ID, attachmentType) if err != nil { return nil, err } diff --git a/backend/internal/services/service_items_test.go b/backend/internal/services/service_items_test.go index 9e6dabd..b6585d4 100644 --- a/backend/internal/services/service_items_test.go +++ b/backend/internal/services/service_items_test.go @@ -126,7 +126,7 @@ func TestItemService_AddAttachment(t *testing.T) { reader := strings.NewReader(contents) // Setup - afterAttachment, err := svc.AddAttachment(context.Background(), tGroup.ID, itm.ID, "testfile.txt", reader) + afterAttachment, err := svc.AddAttachment(context.Background(), tGroup.ID, itm.ID, "testfile.txt", "attachment", reader) assert.NoError(t, err) assert.NotNil(t, afterAttachment) diff --git a/backend/internal/types/item_types.go b/backend/internal/types/item_types.go index ae682ae..df49f0b 100644 --- a/backend/internal/types/item_types.go +++ b/backend/internal/types/item_types.go @@ -103,5 +103,6 @@ type ItemAttachment struct { ID uuid.UUID `json:"id"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` + Type string `json:"type"` Document DocumentOut `json:"document"` } diff --git a/backend/internal/types/label_types.go b/backend/internal/types/label_types.go index 301a3fd..725361c 100644 --- a/backend/internal/types/label_types.go +++ b/backend/internal/types/label_types.go @@ -21,7 +21,6 @@ type LabelUpdate struct { type LabelSummary struct { ID uuid.UUID `json:"id"` - GroupID uuid.UUID `json:"groupId"` Name string `json:"name"` Description string `json:"description"` CreatedAt time.Time `json:"createdAt"` diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 4d0d9aa..09cb676 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -45,6 +45,7 @@ export interface ItemAttachment { createdAt: Date; document: DocumentOut; id: string; + type: string; updatedAt: Date; }