diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 30ef4f4..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: 2 -updates: - # Fetch and update latest `npm` packages - - package-ecosystem: npm - directory: "/frontend" - schedule: - interval: daily - time: "00:00" - open-pull-requests-limit: 10 - reviewers: - - hay-kot - assignees: - - hay-kot - commit-message: - prefix: fix - prefix-development: chore - include: scope - - package-ecosystem: gomod - directory: backend - schedule: - interval: daily - time: "00:00" - open-pull-requests-limit: 10 - reviewers: - - hay-kot - assignees: - - hay-kot - commit-message: - prefix: fix - prefix-development: chore - include: scope diff --git a/.gitignore b/.gitignore index 7fbecd2..05e4ebd 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ node_modules .output .env dist + +.pnpm-store +backend/app/api/app +backend/app/api/__debug_bin \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d375395 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,47 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "compounds": [ + { + "name": "Full Stack", + "configurations": ["Launch Backend", "Launch Frontend"], + "stopAll": true + } + ], + "configurations": [ + { + "name": "Launch Backend", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceRoot}/backend/app/api/", + "args": [], + "env": { + "HBOX_DEMO": "true", + "HBOX_LOG_LEVEL": "debug", + "HBOX_DEBUG_ENABLED": "true", + "HBOX_STORAGE_DATA": "${workspaceRoot}/backend/.data", + "HBOX_STORAGE_SQLITE_URL": "${workspaceRoot}/backend/.data/homebox.db?_fk=1" + }, + }, + { + "name": "Launch Frontend", + "type": "node", + "request": "launch", + "runtimeExecutable": "pnpm", + "runtimeArgs": [ + "run", + "dev" + ], + "cwd": "${workspaceFolder}/frontend", + "serverReadyAction": { + "action": "debugWithChrome", + "pattern": "Local: http://localhost:([0-9]+)", + "uriFormat": "http://localhost:%s", + "webRoot": "${workspaceFolder}/frontend" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3c39b0e..5080f25 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,7 +12,7 @@ "debughandlers" ], // use ESLint to format code on save - "editor.formatOnSave": true, + "editor.formatOnSave": false, "editor.defaultFormatter": "dbaeumer.vscode-eslint", "editor.codeActionsOnSave": { "source.fixAll.eslint": true diff --git a/backend/app/api/handlers/v1/v1_ctrl_assets.go b/backend/app/api/handlers/v1/v1_ctrl_assets.go new file mode 100644 index 0000000..82653fe --- /dev/null +++ b/backend/app/api/handlers/v1/v1_ctrl_assets.go @@ -0,0 +1,61 @@ +package v1 + +import ( + "net/http" + "strconv" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/hay-kot/homebox/backend/internal/core/services" + "github.com/hay-kot/homebox/backend/internal/data/repo" + "github.com/hay-kot/homebox/backend/internal/sys/validate" + "github.com/hay-kot/homebox/backend/pkgs/server" + + "github.com/rs/zerolog/log" +) + +// HandleItemGet godocs +// @Summary Gets an item by Asset ID +// @Tags Assets +// @Produce json +// @Param id path string true "Asset ID" +// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{} +// @Router /v1/assets/{id} [GET] +// @Security Bearer +func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + ctx := services.NewContext(r.Context()) + assetIdParam := chi.URLParam(r, "id") + assetIdParam = strings.ReplaceAll(assetIdParam, "-", "") // Remove dashes + // Convert the asset ID to an int64 + assetId, err := strconv.ParseInt(assetIdParam, 10, 64) + if err != nil { + return err + } + pageParam := r.URL.Query().Get("page") + var page int64 = -1 + if pageParam != "" { + page, err = strconv.ParseInt(pageParam, 10, 64) + if err != nil { + return server.Respond(w, http.StatusBadRequest, "Invalid page number") + } + } + + pageSizeParam := r.URL.Query().Get("pageSize") + var pageSize int64 = -1 + if pageSizeParam != "" { + pageSize, err = strconv.ParseInt(pageSizeParam, 10, 64) + if err != nil { + return server.Respond(w, http.StatusBadRequest, "Invalid page size") + } + } + + + items, err := ctrl.repo.Items.QueryByAssetID(r.Context(), ctx.GID, repo.AssetID(assetId), int(page), int(pageSize)) + if err != nil { + log.Err(err).Msg("failed to get item") + return validate.NewRequestError(err, http.StatusInternalServerError) + } + return server.Respond(w, http.StatusOK, items) + } +} diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 61e13d4..9c71d0f 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -117,6 +117,8 @@ func (a *app) mountRoutes(repos *repo.AllRepos) { a.server.Put(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...) a.server.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryDelete(), userMW...) + a.server.Get(v1Base("/asset/{id}"), v1Ctrl.HandleAssetGet(), userMW...) + a.server.Get( v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentGet(), diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 7863b0d..466f3f5 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -45,6 +45,39 @@ const docTemplate = `{ } } }, + "/v1/assets/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Gets an item by Asset ID", + "parameters": [ + { + "type": "string", + "description": "Asset ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index cccc5f0..2693a8b 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -37,6 +37,39 @@ } } }, + "/v1/assets/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Assets" + ], + "summary": "Gets an item by Asset ID", + "parameters": [ + { + "type": "string", + "description": "Asset ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/repo.PaginationResult-repo_ItemSummary" + } + } + } + } + }, "/v1/groups": { "get": { "security": [ diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index 2efdf42..7be39e2 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -633,6 +633,26 @@ paths: summary: Get the current user tags: - Group + /v1/assets/{id}: + get: + parameters: + - description: Asset ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/repo.PaginationResult-repo_ItemSummary' + security: + - Bearer: [] + summary: Gets an item by Asset ID + tags: + - Assets /v1/groups: get: produces: diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index e22f30a..f5ddb4e 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -356,6 +356,41 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite } + +// QueryByAssetID returns items by asset ID. If the item does not exist, an error is returned. +func (e *ItemsRepository) QueryByAssetID(ctx context.Context, gid uuid.UUID, assetID AssetID, page int, pageSize int) (PaginationResult[ItemSummary], error) { + qb := e.db.Item.Query().Where( + item.HasGroupWith(group.ID(gid)), + item.AssetID(int(assetID)), + ) + + if page != -1 || pageSize != -1 { + qb.Offset(calculateOffset(page, pageSize)). + Limit(pageSize) + } else { + page = -1 + pageSize = -1 + } + + items, err := mapItemsSummaryErr( + qb.Order(ent.Asc(item.FieldName)). + WithLabel(). + WithLocation(). + All(ctx), + ) + + if err != nil { + return PaginationResult[ItemSummary]{}, err + } + + return PaginationResult[ItemSummary]{ + Page: page, + PageSize: pageSize, + Total: len(items), + Items: items, + }, nil +} + // GetAll returns all the items in the database with the Labels and Locations eager loaded. func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSummary, error) { return mapItemsSummaryErr(e.db.Item.Query(). diff --git a/frontend/lib/api/classes/assets.ts b/frontend/lib/api/classes/assets.ts new file mode 100644 index 0000000..8820bb3 --- /dev/null +++ b/frontend/lib/api/classes/assets.ts @@ -0,0 +1,11 @@ +import { BaseAPI, route } from "../base"; +import { ItemSummary } from "../types/data-contracts"; +import { PaginationResult } from "../types/non-generated"; + +export class AssetsApi extends BaseAPI { + async get(id: string, page = 1, pageSize = 50) { + return await this.http.get>({ + url: route(`/asset/${id}`, { page, pageSize }), + }); + } +} diff --git a/frontend/lib/api/user.ts b/frontend/lib/api/user.ts index 079f113..a0fd413 100644 --- a/frontend/lib/api/user.ts +++ b/frontend/lib/api/user.ts @@ -6,6 +6,7 @@ import { GroupApi } from "./classes/group"; import { UserApi } from "./classes/users"; import { ActionsAPI } from "./classes/actions"; import { StatsAPI } from "./classes/stats"; +import { AssetsApi } from "./classes/assets"; import { Requests } from "~~/lib/requests"; export class UserClient extends BaseAPI { @@ -16,17 +17,19 @@ export class UserClient extends BaseAPI { user: UserApi; actions: ActionsAPI; stats: StatsAPI; + assets: AssetsApi; constructor(requests: Requests, attachmentToken: string) { super(requests, attachmentToken); this.locations = new LocationsApi(requests); this.labels = new LabelsApi(requests); - this.items = new ItemsApi(requests); + this.items = new ItemsApi(requests, attachmentToken); this.group = new GroupApi(requests); this.user = new UserApi(requests); this.actions = new ActionsAPI(requests); this.stats = new StatsAPI(requests); + this.assets = new AssetsApi(requests); Object.freeze(this); } diff --git a/frontend/pages/a/[id].vue b/frontend/pages/a/[id].vue new file mode 100644 index 0000000..6bcca5b --- /dev/null +++ b/frontend/pages/a/[id].vue @@ -0,0 +1,9 @@ + diff --git a/frontend/pages/assets/[id].vue b/frontend/pages/assets/[id].vue new file mode 100644 index 0000000..841a1cd --- /dev/null +++ b/frontend/pages/assets/[id].vue @@ -0,0 +1,42 @@ + + +