mirror of
https://github.com/hay-kot/homebox.git
synced 2024-12-18 13:06:32 +00:00
feat: add lookup by asset ID (#208)
* add asset id redirecting * dev env changes * suggested changes from PR * remove unnecessary proxy from nuxt config * fix formatting * change directory reference * fix API key storage * use /a/{id} as redirect * run generators * remove dependabot Co-authored-by: Bradley Nelson <bradley@nel.family> Co-authored-by: Bradley Nelson <BCNelson@users.noreply.github.com>
This commit is contained in:
parent
c78f10ba5d
commit
07441eec8e
14 changed files with 302 additions and 33 deletions
31
.github/dependabot.yml
vendored
31
.github/dependabot.yml
vendored
|
@ -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
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -45,3 +45,7 @@ node_modules
|
||||||
.output
|
.output
|
||||||
.env
|
.env
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
.pnpm-store
|
||||||
|
backend/app/api/app
|
||||||
|
backend/app/api/__debug_bin
|
47
.vscode/launch.json
vendored
Normal file
47
.vscode/launch.json
vendored
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -12,7 +12,7 @@
|
||||||
"debughandlers"
|
"debughandlers"
|
||||||
],
|
],
|
||||||
// use ESLint to format code on save
|
// use ESLint to format code on save
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": false,
|
||||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": true
|
||||||
|
|
61
backend/app/api/handlers/v1/v1_ctrl_assets.go
Normal file
61
backend/app/api/handlers/v1/v1_ctrl_assets.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.Put(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...)
|
||||||
a.server.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryDelete(), 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(
|
a.server.Get(
|
||||||
v1Base("/items/{id}/attachments/{attachment_id}"),
|
v1Base("/items/{id}/attachments/{attachment_id}"),
|
||||||
v1Ctrl.HandleItemAttachmentGet(),
|
v1Ctrl.HandleItemAttachmentGet(),
|
||||||
|
|
|
@ -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": {
|
"/v1/groups": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
|
@ -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": {
|
"/v1/groups": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
|
@ -633,6 +633,26 @@ paths:
|
||||||
summary: Get the current user
|
summary: Get the current user
|
||||||
tags:
|
tags:
|
||||||
- Group
|
- 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:
|
/v1/groups:
|
||||||
get:
|
get:
|
||||||
produces:
|
produces:
|
||||||
|
|
|
@ -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.
|
// 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) {
|
func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSummary, error) {
|
||||||
return mapItemsSummaryErr(e.db.Item.Query().
|
return mapItemsSummaryErr(e.db.Item.Query().
|
||||||
|
|
11
frontend/lib/api/classes/assets.ts
Normal file
11
frontend/lib/api/classes/assets.ts
Normal file
|
@ -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<PaginationResult<ItemSummary>>({
|
||||||
|
url: route(`/asset/${id}`, { page, pageSize }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import { GroupApi } from "./classes/group";
|
||||||
import { UserApi } from "./classes/users";
|
import { UserApi } from "./classes/users";
|
||||||
import { ActionsAPI } from "./classes/actions";
|
import { ActionsAPI } from "./classes/actions";
|
||||||
import { StatsAPI } from "./classes/stats";
|
import { StatsAPI } from "./classes/stats";
|
||||||
|
import { AssetsApi } from "./classes/assets";
|
||||||
import { Requests } from "~~/lib/requests";
|
import { Requests } from "~~/lib/requests";
|
||||||
|
|
||||||
export class UserClient extends BaseAPI {
|
export class UserClient extends BaseAPI {
|
||||||
|
@ -16,17 +17,19 @@ export class UserClient extends BaseAPI {
|
||||||
user: UserApi;
|
user: UserApi;
|
||||||
actions: ActionsAPI;
|
actions: ActionsAPI;
|
||||||
stats: StatsAPI;
|
stats: StatsAPI;
|
||||||
|
assets: AssetsApi;
|
||||||
|
|
||||||
constructor(requests: Requests, attachmentToken: string) {
|
constructor(requests: Requests, attachmentToken: string) {
|
||||||
super(requests, attachmentToken);
|
super(requests, attachmentToken);
|
||||||
|
|
||||||
this.locations = new LocationsApi(requests);
|
this.locations = new LocationsApi(requests);
|
||||||
this.labels = new LabelsApi(requests);
|
this.labels = new LabelsApi(requests);
|
||||||
this.items = new ItemsApi(requests);
|
this.items = new ItemsApi(requests, attachmentToken);
|
||||||
this.group = new GroupApi(requests);
|
this.group = new GroupApi(requests);
|
||||||
this.user = new UserApi(requests);
|
this.user = new UserApi(requests);
|
||||||
this.actions = new ActionsAPI(requests);
|
this.actions = new ActionsAPI(requests);
|
||||||
this.stats = new StatsAPI(requests);
|
this.stats = new StatsAPI(requests);
|
||||||
|
this.assets = new AssetsApi(requests);
|
||||||
|
|
||||||
Object.freeze(this);
|
Object.freeze(this);
|
||||||
}
|
}
|
||||||
|
|
9
frontend/pages/a/[id].vue
Normal file
9
frontend/pages/a/[id].vue
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ["auth"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const assetId = computed<string>(() => route.params.id as string);
|
||||||
|
await navigateTo("/assets/" + assetId.value, { replace: true, redirectCode: 301 });
|
||||||
|
</script>
|
42
frontend/pages/assets/[id].vue
Normal file
42
frontend/pages/assets/[id].vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ["auth"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const api = useUserApi();
|
||||||
|
const toast = useNotifier();
|
||||||
|
|
||||||
|
const assetId = computed<string>(() => route.params.id as string);
|
||||||
|
|
||||||
|
const { pending, data: items } = useLazyAsyncData(`asset/${assetId.value}`, async () => {
|
||||||
|
const { data, error } = await api.assets.get(assetId.value);
|
||||||
|
if (error) {
|
||||||
|
toast.error("Failed to load asset");
|
||||||
|
navigateTo("/home");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (data.total) {
|
||||||
|
case 0:
|
||||||
|
toast.error("Asset not found");
|
||||||
|
navigateTo("/home");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
navigateTo(`/item/${data.items[0].id}`, { replace: true, redirectCode: 302 });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return data.items;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseContainer>
|
||||||
|
<section v-if="!pending">
|
||||||
|
<BaseSectionHeader class="mb-5"> This Asset Id is associated with multiple items</BaseSectionHeader>
|
||||||
|
<div class="grid gap-2 grid-cols-1 sm:grid-cols-2">
|
||||||
|
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseContainer>
|
||||||
|
</template>
|
Loading…
Reference in a new issue