Compare commits

..

1 commit

Author SHA1 Message Date
Hayden
9d182d07b0
fix label store 2023-02-18 09:41:48 -09:00
309 changed files with 9482 additions and 23563 deletions

View file

@ -35,6 +35,6 @@
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node", "remoteUser": "node",
"features": { "features": {
"golang": "1.20" "golang": "1.19"
} }
} }

View file

@ -22,4 +22,3 @@
**/secrets.dev.yaml **/secrets.dev.yaml
**/values.dev.yaml **/values.dev.yaml
README.md README.md
!Dockerfile.rootless

View file

@ -10,9 +10,9 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v3
with: with:
go-version: "1.20" go-version: 1.19
- name: Install Task - name: Install Task
uses: arduino/setup-task@v1 uses: arduino/setup-task@v1

View file

@ -44,9 +44,9 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v3
with: with:
go-version: "1.20" go-version: 1.19
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:

View file

@ -22,9 +22,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v3
with: with:
go-version: "1.20" go-version: 1.19
- name: Set up QEMU - name: Set up QEMU
id: qemu id: qemu
@ -44,7 +44,7 @@ jobs:
env: env:
CR_PAT: ${{ secrets.GH_TOKEN }} CR_PAT: ${{ secrets.GH_TOKEN }}
- name: build nightly image - name: build nightly the image
if: ${{ inputs.release == false }} if: ${{ inputs.release == false }}
run: | run: |
docker build --push --no-cache \ docker build --push --no-cache \
@ -53,16 +53,6 @@ jobs:
--build-arg=BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ --build-arg=BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--platform=linux/amd64,linux/arm64,linux/arm/v7 . --platform=linux/amd64,linux/arm64,linux/arm/v7 .
- name: build nightly-rootless image
if: ${{ inputs.release == false }}
run: |
docker build --push --no-cache \
--tag=ghcr.io/hay-kot/homebox:${{ inputs.tag }}-rootless \
--build-arg=COMMIT=$(git rev-parse HEAD) \
--build-arg=BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--file Dockerfile.rootless \
--platform=linux/amd64,linux/arm64,linux/arm/v7 .
- name: build release tagged the image - name: build release tagged the image
if: ${{ inputs.release == true }} if: ${{ inputs.release == true }}
run: | run: |
@ -74,16 +64,3 @@ jobs:
--build-arg COMMIT=$(git rev-parse HEAD) \ --build-arg COMMIT=$(git rev-parse HEAD) \
--build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ --build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--platform linux/amd64,linux/arm64,linux/arm/v7 . --platform linux/amd64,linux/arm64,linux/arm/v7 .
- name: build release tagged the rootless image
if: ${{ inputs.release == true }}
run: |
docker build --push --no-cache \
--tag ghcr.io/hay-kot/homebox:nightly-rootless \
--tag ghcr.io/hay-kot/homebox:latest-rootless \
--tag ghcr.io/hay-kot/homebox:${{ inputs.tag }}-rootless \
--build-arg VERSION=${{ inputs.tag }} \
--build-arg COMMIT=$(git rev-parse HEAD) \
--build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--file Dockerfile.rootless .

View file

@ -1,17 +1,31 @@
name: Publish Dockers name: Build Nightly
on: on:
push: push:
branches: branches:
- main - main
release:
types:
- published
env: env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs: jobs:
backend-tests:
name: "Backend Server Tests"
uses: hay-kot/homebox/.github/workflows/partial-backend.yaml@main
frontend-tests:
name: "Frontend and End-to-End Tests"
uses: hay-kot/homebox/.github/workflows/partial-frontend.yaml@main
deploy: deploy:
name: "Deploy Nightly to Fly.io" name: "Deploy Nightly to Fly.io"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs:
- backend-tests
- frontend-tests
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master - uses: superfly/flyctl-actions/setup-flyctl@master
@ -20,10 +34,40 @@ jobs:
publish-nightly: publish-nightly:
name: "Publish Nightly" name: "Publish Nightly"
if: github.event_name != 'release' if: github.event_name != 'release'
needs:
- backend-tests
- frontend-tests
uses: hay-kot/homebox/.github/workflows/partial-publish.yaml@main uses: hay-kot/homebox/.github/workflows/partial-publish.yaml@main
with: with:
tag: nightly tag: nightly
secrets: secrets:
GH_TOKEN: ${{ secrets.CR_PAT }} GH_TOKEN: ${{ secrets.CR_PAT }}
publish-tag:
name: "Publish Tag"
if: github.event_name == 'release'
needs:
- backend-tests
- frontend-tests
uses: hay-kot/homebox/.github/workflows/partial-publish.yaml@main
with:
release: true
tag: ${{ github.event.release.tag_name }}
secrets:
GH_TOKEN: ${{ secrets.CR_PAT }}
deploy-docs:
name: Deploy docs
needs:
- publish-tag
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v3
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CONFIG_FILE: docs/mkdocs.yml
EXTRA_PACKAGES: build-base

View file

@ -1,77 +0,0 @@
name: Publish Release
on:
push:
tags:
- v*
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs:
backend-tests:
name: "Backend Server Tests"
uses: hay-kot/homebox/.github/workflows/partial-backend.yaml@main
frontend-tests:
name: "Frontend and End-to-End Tests"
uses: hay-kot/homebox/.github/workflows/partial-frontend.yaml@main
goreleaser:
name: goreleaser
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
- uses: pnpm/action-setup@v2
with:
version: 7.30.1
- name: Build Frontend and Copy to Backend
working-directory: frontend
run: |
pnpm install --shamefully-hoist
pnpm run build
cp -r ./.output/public ../backend/app/api/static/
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
with:
workdir: "backend"
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-tag:
name: "Publish Tag"
uses: hay-kot/homebox/.github/workflows/partial-publish.yaml@main
with:
release: true
tag: ${{ github.ref_name }}
secrets:
GH_TOKEN: ${{ secrets.CR_PAT }}
deploy-docs:
name: Deploy docs
needs:
- publish-tag
- goreleaser
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v3
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CONFIG_FILE: docs/mkdocs.yml
EXTRA_PACKAGES: build-base

5
.gitignore vendored
View file

@ -49,8 +49,3 @@ dist
.pnpm-store .pnpm-store
backend/app/api/app backend/app/api/app
backend/app/api/__debug_bin backend/app/api/__debug_bin
dist/
# Nuxt Publish Dir
backend/app/api/static/public/*
!backend/app/api/static/public/.gitkeep

View file

@ -1,33 +0,0 @@
---
# yaml-language-server: $schema=https://hay-kot.github.io/scaffold/schema.json
messages:
pre: |
# Ent Model Generation
With Boilerplate!
post: |
Complete!
questions:
- name: "model"
prompt:
message: "What is the name of the model? (PascalCase)"
required: true
- name: "by_group"
prompt:
confirm: "Include a Group Edge? (group_id -> id)"
required: true
rewrites:
- from: 'templates/model.go'
to: 'backend/internal/data/ent/schema/{{ lower .Scaffold.model }}.go'
inject:
- name: "Insert Groups Edge"
path: 'backend/internal/data/ent/schema/group.go'
at: // $scaffold_edge
template: |
{{- if .Scaffold.by_group -}}
owned("{{ lower .Scaffold.model }}s", {{ .Scaffold.model }}.Type),
{{- end -}}

View file

@ -1,40 +0,0 @@
package schema
import (
"entgo.io/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/schema/mixins"
)
type {{ .Scaffold.model }} struct {
ent.Schema
}
func ({{ .Scaffold.model }}) Mixin() []ent.Mixin {
return []ent.Mixin{
mixins.BaseMixin{},
{{- if .Scaffold.by_group }}
GroupMixin{ref: "{{ snakecase .Scaffold.model }}s"},
{{- end }}
}
}
// Fields of the {{ .Scaffold.model }}.
func ({{ .Scaffold.model }}) Fields() []ent.Field {
return []ent.Field{
// field.String("name").
}
}
// Edges of the {{ .Scaffold.model }}.
func ({{ .Scaffold.model }}) Edges() []ent.Edge {
return []ent.Edge{
// edge.From("group", Group.Type).
}
}
func ({{ .Scaffold.model }}) Indexes() []ent.Index {
return []ent.Index{
// index.Fields("token"),
}
}

View file

@ -49,9 +49,3 @@ start command `task: ui:dev`
1. The frontend is a Vue 3 app with Nuxt.js that uses Tailwind and DaisyUI for styling. 1. The frontend is a Vue 3 app with Nuxt.js that uses Tailwind and DaisyUI for styling.
2. We're using Vitest for our automated testing. you can run these with `task ui:watch`. 2. We're using Vitest for our automated testing. you can run these with `task ui:watch`.
3. Tests require the API server to be running and in some cases the first run will fail due to a race condition. If this happens just run the tests again and they should pass. 3. Tests require the API server to be running and in some cases the first run will fail due to a race condition. If this happens just run the tests again and they should pass.
## Publishing Release
Create a new tag in github with the version number vX.X.X. This will trigger a new release to be created.
Test -> Goreleaser -> Publish Release -> Trigger Docker Builds -> Deploy Docs + Fly.io Demo

View file

@ -1,6 +1,6 @@
# Build Nuxt # Build Nuxt
FROM r.batts.cloud/node:18 as frontend-builder FROM node:17-alpine as frontend-builder
WORKDIR /app WORKDIR /app
RUN npm install -g pnpm RUN npm install -g pnpm
COPY frontend/package.json frontend/pnpm-lock.yaml ./ COPY frontend/package.json frontend/pnpm-lock.yaml ./
@ -9,30 +9,32 @@ COPY frontend .
RUN pnpm build RUN pnpm build
# Build API # Build API
FROM r.batts.cloud/golang:1.21 AS builder FROM golang:alpine AS builder
ARG BUILD_TIME ARG BUILD_TIME
ARG COMMIT ARG COMMIT
ARG VERSION ARG VERSION
RUN apt update && \ RUN apk update && \
apt install -y git build-essential gcc g++ apk upgrade && \
apk add --update git build-base gcc g++
WORKDIR /go/src/app WORKDIR /go/src/app
COPY ./backend . COPY ./backend .
RUN go get -d -v ./... RUN go get -d -v ./...
RUN rm -rf ./app/api/public RUN rm -rf ./app/api/public
COPY --from=frontend-builder /app/.output/public ./app/api/static/public COPY --from=frontend-builder /app/.output/public ./app/api/static/public
RUN CGO_ENABLED=0 GOOS=linux go build \ RUN CGO_ENABLED=1 GOOS=linux go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \ -ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api \ -o /go/bin/api \
-v ./app/api/*.go -v ./app/api/*.go
# Production Stage # Production Stage
FROM r.batts.cloud/debian:bookworm FROM alpine:latest
ENV HBOX_MODE=production ENV HBOX_MODE=production
ENV HBOX_STORAGE_DATA=/data/ ENV HBOX_STORAGE_DATA=/data/
ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_fk=1 ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_fk=1
RUN apk --no-cache add ca-certificates
RUN mkdir /app RUN mkdir /app
COPY --from=builder /go/bin/api /app COPY --from=builder /go/bin/api /app

View file

@ -1,53 +0,0 @@
# Build Nuxt
FROM node:17-alpine as frontend-builder
WORKDIR /app
RUN npm install -g pnpm
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --shamefully-hoist
COPY frontend .
RUN pnpm build
# Build API
FROM golang:alpine AS builder
ARG BUILD_TIME
ARG COMMIT
ARG VERSION
RUN apk update && \
apk upgrade && \
apk add --update git build-base gcc g++
WORKDIR /go/src/app
COPY ./backend .
RUN go get -d -v ./...
RUN rm -rf ./app/api/public
COPY --from=frontend-builder /app/.output/public ./app/api/static/public
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-s -w -X main.commit=$COMMIT -X main.buildTime=$BUILD_TIME -X main.version=$VERSION" \
-o /go/bin/api \
-v ./app/api/*.go && \
chmod +x /go/bin/api && \
# create a directory so that we can copy it in the next stage
mkdir /data
# Production Stage
FROM gcr.io/distroless/static
ENV HBOX_MODE=production
ENV HBOX_STORAGE_DATA=/data/
ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_fk=1
# Copy the binary and the (empty) /data dir and
# change the ownership to the low-privileged user
COPY --from=builder --chown=nonroot /go/bin/api /app
COPY --from=builder --chown=nonroot /data /data
LABEL Name=homebox Version=0.0.1
LABEL org.opencontainers.image.source="https://github.com/hay-kot/homebox"
EXPOSE 7745
VOLUME [ "/data" ]
# Drop root and run as low-privileged user
USER nonroot
ENTRYPOINT [ "/app" ]
CMD [ "/data/config.yml" ]

View file

@ -16,18 +16,10 @@
[Configuration & Docker Compose](https://hay-kot.github.io/homebox/quick-start) [Configuration & Docker Compose](https://hay-kot.github.io/homebox/quick-start)
```bash ```bash
# If using the rootless image, ensure data docker run --name=homebox \
# folder has correct permissions --restart=always \
mkdir -p /path/to/data/folder --publish=3100:7745 \
chown 65532:65532 -R /path/to/data/folder
docker run -d \
--name homebox \
--restart unless-stopped \
--publish 3100:7745 \
--env TZ=Europe/Bucharest \
--volume /path/to/data/folder/:/data \
ghcr.io/hay-kot/homebox:latest ghcr.io/hay-kot/homebox:latest
# ghcr.io/hay-kot/homebox:latest-rootless
``` ```
## Credits ## Credits

View file

@ -1,7 +1,7 @@
version: "3" version: "3"
env: env:
HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1 HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_fk=1
HBOX_OPTIONS_ALLOW_REGISTRATION: true HBOX_OPTIONS_ALLOW_REGISTRATION: true
UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure" UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure"
tasks: tasks:
@ -27,47 +27,46 @@ tasks:
--modular \ --modular \
--path ./backend/app/api/static/docs/swagger.json \ --path ./backend/app/api/static/docs/swagger.json \
--output ./frontend/lib/api/types --output ./frontend/lib/api/types
- go run ./backend/app/tools/typegen/main.go ./frontend/lib/api/types/data-contracts.ts - go run ./scripts/process-types/*.go ./frontend/lib/api/types/data-contracts.ts
- cp ./backend/app/api/static/docs/swagger.json docs/docs/api/openapi-2.0.json
sources: sources:
- "./backend/app/api/**/*" - "./backend/app/api/**/*"
- "./backend/internal/data/**" - "./backend/internal/data/**"
- "./backend/internal/core/services/**/*" - "./backend/internal/services/**/*"
- "./backend/app/tools/typegen/main.go" - "./scripts/process-types.py"
generates:
- "./frontend/lib/api/types/data-contracts.ts"
- "./backend/internal/data/ent/schema"
- "./backend/app/api/static/docs/swagger.json"
- "./backend/app/api/static/docs/swagger.yaml"
go:run: go:run:
desc: Starts the backend api server (depends on generate task) desc: Starts the backend api server (depends on generate task)
dir: backend
deps: deps:
- generate - generate
cmds: cmds:
- go run ./app/api/ {{ .CLI_ARGS }} - cd backend && go run ./app/api/ {{ .CLI_ARGS }}
silent: false silent: false
go:test: go:test:
desc: Runs all go tests using gotestsum - supports passing gotestsum args desc: Runs all go tests using gotestsum - supports passing gotestsum args
dir: backend
cmds: cmds:
- gotestsum {{ .CLI_ARGS }} ./... - cd backend && gotestsum {{ .CLI_ARGS }} ./...
go:coverage: go:coverage:
desc: Runs all go tests with -race flag and generates a coverage report desc: Runs all go tests with -race flag and generates a coverage report
dir: backend
cmds: cmds:
- go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover - cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover
silent: true silent: true
go:tidy: go:tidy:
desc: Runs go mod tidy on the backend desc: Runs go mod tidy on the backend
dir: backend
cmds: cmds:
- go mod tidy - cd backend && go mod tidy
go:lint: go:lint:
desc: Runs golangci-lint desc: Runs golangci-lint
dir: backend
cmds: cmds:
- golangci-lint run ./... - cd backend && golangci-lint run ./...
go:all: go:all:
desc: Runs all go test and lint related tasks desc: Runs all go test and lint related tasks
@ -78,19 +77,19 @@ tasks:
go:build: go:build:
desc: Builds the backend binary desc: Builds the backend binary
dir: backend
cmds: cmds:
- go build -o ../build/backend ./app/api - cd backend && go build -o ../build/backend ./app/api
db:generate: db:generate:
desc: Run Entgo.io Code Generation desc: Run Entgo.io Code Generation
dir: backend/internal/
cmds: cmds:
- | - |
go generate ./... \ cd backend/internal/ && go generate ./... \
--template=./data/ent/schema/templates/has_id.tmpl --template=./data/ent/schema/templates/has_id.tmpl
sources: sources:
- "./backend/internal/data/ent/schema/**/*" - "./backend/internal/data/ent/schema/**/*"
generates:
- "./backend/internal/ent/"
db:migration: db:migration:
desc: Runs the database diff engine to generate a SQL migration files desc: Runs the database diff engine to generate a SQL migration files
@ -101,27 +100,23 @@ tasks:
ui:watch: ui:watch:
desc: Starts the vitest test runner in watch mode desc: Starts the vitest test runner in watch mode
dir: frontend
cmds: cmds:
- pnpm run test:watch - cd frontend && pnpm run test:watch
ui:dev: ui:dev:
desc: Run frontend development server desc: Run frontend development server
dir: frontend
cmds: cmds:
- pnpm dev - cd frontend && pnpm dev
ui:fix: ui:fix:
desc: Runs prettier and eslint on the frontend desc: Runs prettier and eslint on the frontend
dir: frontend
cmds: cmds:
- pnpm run lint:fix - cd frontend && pnpm run lint:fix
ui:check: ui:check:
desc: Runs type checking desc: Runs type checking
dir: frontend
cmds: cmds:
- pnpm run typecheck - cd frontend && pnpm run typecheck
test:ci: test:ci:
desc: Runs end-to-end test on a live server (only for use in CI) desc: Runs end-to-end test on a live server (only for use in CI)

2
backend/.gitignore vendored
View file

@ -1,2 +0,0 @@
dist/

View file

@ -1,54 +0,0 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# you may remove this if you don't need go generate
- go generate ./...
builds:
- main: ./app/api
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- "386"
- arm
- arm64
ignore:
- goos: windows
goarch: arm
- goos: windows
goarch: "386"
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

Binary file not shown.

View file

@ -4,12 +4,11 @@ import (
"time" "time"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/config" "github.com/hay-kot/homebox/backend/internal/sys/config"
"github.com/hay-kot/homebox/backend/pkgs/mailer" "github.com/hay-kot/homebox/backend/pkgs/mailer"
"github.com/hay-kot/httpkit/server" "github.com/hay-kot/homebox/backend/pkgs/server"
) )
type app struct { type app struct {
@ -19,7 +18,6 @@ type app struct {
server *server.Server server *server.Server
repos *repo.AllRepos repos *repo.AllRepos
services *services.AllServices services *services.AllServices
bus *eventbus.EventBus
} }
func new(conf *config.Config) *app { func new(conf *config.Config) *app {
@ -39,11 +37,8 @@ func new(conf *config.Config) *app {
} }
func (a *app) startBgTask(t time.Duration, fn func()) { func (a *app) startBgTask(t time.Duration, fn func()) {
timer := time.NewTimer(t)
for { for {
timer.Reset(t)
a.server.Background(fn) a.server.Background(fn)
<-timer.C time.Sleep(t)
} }
} }

View file

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"encoding/csv"
"strings" "strings"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
@ -9,7 +10,7 @@ import (
) )
func (a *app) SetupDemo() { func (a *app) SetupDemo() {
csvText := `HB.import_ref,HB.location,HB.labels,HB.quantity,HB.name,HB.description,HB.insured,HB.serial_number,HB.model_number,HB.manufacturer,HB.notes,HB.purchase_from,HB.purchase_price,HB.purchase_time,HB.lifetime_warranty,HB.warranty_expires,HB.warranty_details,HB.sold_to,HB.sold_price,HB.sold_time,HB.sold_notes csvText := `Import Ref,Location,Labels,Quantity,Name,Description,Insured,Serial Number,Model Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased Time,Lifetime Warranty,Warranty Expires,Warranty Details,Sold To,Sold Price,Sold Time,Sold Notes
,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,, ,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,, ,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,,
,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,, ,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
@ -18,14 +19,16 @@ func (a *app) SetupDemo() {
,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,, ,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,,
` `
registration := services.UserRegistration{ var (
registration = services.UserRegistration{
Email: "demo@example.com", Email: "demo@example.com",
Name: "Demo", Name: "Demo",
Password: "demo", Password: "demo",
} }
)
// First check if we've already setup a demo user and skip if so // First check if we've already setup a demo user and skip if so
_, err := a.services.User.Login(context.Background(), registration.Email, registration.Password, false) _, err := a.services.User.Login(context.Background(), registration.Email, registration.Password)
if err == nil { if err == nil {
return return
} }
@ -36,10 +39,20 @@ func (a *app) SetupDemo() {
log.Fatal().Msg("Failed to setup demo") log.Fatal().Msg("Failed to setup demo")
} }
token, _ := a.services.User.Login(context.Background(), registration.Email, registration.Password, false) token, _ := a.services.User.Login(context.Background(), registration.Email, registration.Password)
self, _ := a.services.User.GetSelf(context.Background(), token.Raw) self, _ := a.services.User.GetSelf(context.Background(), token.Raw)
_, err = a.services.Items.CsvImport(context.Background(), self.GroupID, strings.NewReader(csvText)) // Read CSV Text
reader := csv.NewReader(strings.NewReader(csvText))
reader.Comma = ','
records, err := reader.ReadAll()
if err != nil {
log.Err(err).Msg("Failed to read CSV")
log.Fatal().Msg("Failed to setup demo")
}
_, err = a.services.Items.CsvImport(context.Background(), self.GroupID, records)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to import CSV") log.Err(err).Msg("Failed to import CSV")
log.Fatal().Msg("Failed to setup demo") log.Fatal().Msg("Failed to setup demo")

View file

@ -1,36 +1,13 @@
package v1 package v1
import ( import (
"fmt"
"net/http" "net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log"
"github.com/olahol/melody"
) )
type Results[T any] struct {
Items []T `json:"items"`
}
func WrapResults[T any](items []T) Results[T] {
return Results[T]{Items: items}
}
type Wrapped struct {
Item interface{} `json:"item"`
}
func Wrap(v any) Wrapped {
return Wrapped{Item: v}
}
func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) { func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
return func(ctrl *V1Controller) { return func(ctrl *V1Controller) {
ctrl.maxUploadSize = maxUploadSize ctrl.maxUploadSize = maxUploadSize
@ -49,20 +26,12 @@ func WithRegistration(allowRegistration bool) func(*V1Controller) {
} }
} }
func WithSecureCookies(secure bool) func(*V1Controller) {
return func(ctrl *V1Controller) {
ctrl.cookieSecure = secure
}
}
type V1Controller struct { type V1Controller struct {
cookieSecure bool
repo *repo.AllRepos repo *repo.AllRepos
svc *services.AllServices svc *services.AllServices
maxUploadSize int64 maxUploadSize int64
isDemo bool isDemo bool
allowRegistration bool allowRegistration bool
bus *eventbus.EventBus
} }
type ( type (
@ -91,12 +60,11 @@ func BaseUrlFunc(prefix string) func(s string) string {
} }
} }
func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *eventbus.EventBus, options ...func(*V1Controller)) *V1Controller { func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, options ...func(*V1Controller)) *V1Controller {
ctrl := &V1Controller{ ctrl := &V1Controller{
repo: repos, repo: repos,
svc: svc, svc: svc,
allowRegistration: true, allowRegistration: true,
bus: bus,
} }
for _, opt := range options { for _, opt := range options {
@ -107,60 +75,20 @@ func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *event
} }
// HandleBase godoc // HandleBase godoc
// // @Summary Retrieves the basic information about the API
// @Summary Application Info
// @Tags Base // @Tags Base
// @Produce json // @Produce json
// @Success 200 {object} ApiSummary // @Success 200 {object} ApiSummary
// @Router /v1/status [GET] // @Router /v1/status [GET]
func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) errchain.HandlerFunc { func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
return server.JSON(w, http.StatusOK, ApiSummary{ return server.Respond(w, http.StatusOK, ApiSummary{
Healthy: ready(), Healthy: ready(),
Title: "Homebox", Title: "Go API Template",
Message: "Track, Manage, and Organize your Things", Message: "Welcome to the Go API Template Application!",
Build: build, Build: build,
Demo: ctrl.isDemo, Demo: ctrl.isDemo,
AllowRegistration: ctrl.allowRegistration, AllowRegistration: ctrl.allowRegistration,
}) })
} }
} }
func (ctrl *V1Controller) HandleCacheWS() errchain.HandlerFunc {
m := melody.New()
m.HandleConnect(func(s *melody.Session) {
auth := services.NewContext(s.Request.Context())
s.Set("gid", auth.GID)
})
factory := func(e string) func(data any) {
return func(data any) {
eventData, ok := data.(eventbus.GroupMutationEvent)
if !ok {
log.Log().Msgf("invalid event data: %v", data)
return
}
jsonStr := fmt.Sprintf(`{"event": "%s"}`, e)
_ = m.BroadcastFilter([]byte(jsonStr), func(s *melody.Session) bool {
groupIDStr, ok := s.Get("gid")
if !ok {
return false
}
GID := groupIDStr.(uuid.UUID)
return GID == eventData.GID
})
}
}
ctrl.bus.Subscribe(eventbus.EventLabelMutation, factory("label.mutation"))
ctrl.bus.Subscribe(eventbus.EventLocationMutation, factory("location.mutation"))
ctrl.bus.Subscribe(eventbus.EventItemMutation, factory("item.mutation"))
return func(w http.ResponseWriter, r *http.Request) error {
return m.HandleRequest(w, r)
}
}

View file

@ -21,7 +21,7 @@ func (ctrl *V1Controller) routeID(r *http.Request) (uuid.UUID, error) {
func (ctrl *V1Controller) routeUUID(r *http.Request, key string) (uuid.UUID, error) { func (ctrl *V1Controller) routeUUID(r *http.Request, key string) (uuid.UUID, error) {
ID, err := uuid.Parse(chi.URLParam(r, key)) ID, err := uuid.Parse(chi.URLParam(r, key))
if err != nil { if err != nil {
return uuid.Nil, validate.NewRouteKeyError(key) return uuid.Nil, validate.NewInvalidRouteKeyError(key)
} }
return ID, nil return ID, nil
} }

View file

@ -1,14 +1,11 @@
package v1 package v1
import ( import (
"context"
"net/http" "net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -16,68 +13,44 @@ type ActionAmountResult struct {
Completed int `json:"completed"` Completed int `json:"completed"`
} }
func actionHandlerFactory(ref string, fn func(context.Context, uuid.UUID) (int, error)) errchain.HandlerFunc { // HandleGroupInvitationsCreate godoc
return func(w http.ResponseWriter, r *http.Request) error { // @Summary Ensures all items in the database have an asset id
ctx := services.NewContext(r.Context()) // @Tags Group
totalCompleted, err := fn(ctx, ctx.GID)
if err != nil {
log.Err(err).Str("action_ref", ref).Msg("failed to run action")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
}
}
// HandleEnsureAssetID godoc
//
// @Summary Ensure Asset IDs
// @Description Ensures all items in the database have an asset ID
// @Tags Actions
// @Produce json // @Produce json
// @Success 200 {object} ActionAmountResult // @Success 200 {object} ActionAmountResult
// @Router /v1/actions/ensure-asset-ids [Post] // @Router /v1/actions/ensure-asset-ids [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleEnsureAssetID() errchain.HandlerFunc { func (ctrl *V1Controller) HandleEnsureAssetID() server.HandlerFunc {
return actionHandlerFactory("ensure asset IDs", ctrl.svc.Items.EnsureAssetID) return func(w http.ResponseWriter, r *http.Request) error {
} ctx := services.NewContext(r.Context())
// HandleEnsureImportRefs godoc totalCompleted, err := ctrl.svc.Items.EnsureAssetID(ctx, ctx.GID)
// if err != nil {
// @Summary Ensures Import Refs log.Err(err).Msg("failed to ensure asset id")
// @Description Ensures all items in the database have an import ref return validate.NewRequestError(err, http.StatusInternalServerError)
// @Tags Actions }
// @Produce json
// @Success 200 {object} ActionAmountResult return server.Respond(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
// @Router /v1/actions/ensure-import-refs [Post] }
// @Security Bearer
func (ctrl *V1Controller) HandleEnsureImportRefs() errchain.HandlerFunc {
return actionHandlerFactory("ensure import refs", ctrl.svc.Items.EnsureImportRef)
} }
// HandleItemDateZeroOut godoc // HandleItemDateZeroOut godoc
// // @Summary Resets all item date fields to the beginning of the day
// @Summary Zero Out Time Fields // @Tags Group
// @Description Resets all item date fields to the beginning of the day
// @Tags Actions
// @Produce json // @Produce json
// @Success 200 {object} ActionAmountResult // @Success 200 {object} ActionAmountResult
// @Router /v1/actions/zero-item-time-fields [Post] // @Router /v1/actions/zero-item-time-fields [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemDateZeroOut() server.HandlerFunc {
return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields) return func(w http.ResponseWriter, r *http.Request) error {
} ctx := services.NewContext(r.Context())
// HandleSetPrimaryPhotos godoc totalCompleted, err := ctrl.repo.Items.ZeroOutTimeFields(ctx, ctx.GID)
// if err != nil {
// @Summary Set Primary Photos log.Err(err).Msg("failed to ensure asset id")
// @Description Sets the first photo of each item as the primary photo return validate.NewRequestError(err, http.StatusInternalServerError)
// @Tags Actions }
// @Produce json
// @Success 200 {object} ActionAmountResult return server.Respond(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
// @Router /v1/actions/set-primary-photos [Post] }
// @Security Bearer
func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
return actionHandlerFactory("ensure asset IDs", ctrl.repo.Items.SetPrimaryPhotos)
} }

View file

@ -9,22 +9,20 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "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/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// HandleAssetGet godocs // HandleItemGet godocs
// // @Summary Gets an item by Asset ID
// @Summary Get Item by Asset ID // @Tags Assets
// @Tags Items
// @Produce json // @Produce json
// @Param id path string true "Asset ID" // @Param id path string true "Asset ID"
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{} // @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/assets/{id} [GET] // @Router /v1/assets/{id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc { func (ctrl *V1Controller) HandleAssetGet() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context()) ctx := services.NewContext(r.Context())
assetIdParam := chi.URLParam(r, "id") assetIdParam := chi.URLParam(r, "id")
@ -39,7 +37,7 @@ func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
if pageParam != "" { if pageParam != "" {
page, err = strconv.ParseInt(pageParam, 10, 64) page, err = strconv.ParseInt(pageParam, 10, 64)
if err != nil { if err != nil {
return server.JSON(w, http.StatusBadRequest, "Invalid page number") return server.Respond(w, http.StatusBadRequest, "Invalid page number")
} }
} }
@ -48,7 +46,7 @@ func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
if pageSizeParam != "" { if pageSizeParam != "" {
pageSize, err = strconv.ParseInt(pageSizeParam, 10, 64) pageSize, err = strconv.ParseInt(pageSizeParam, 10, 64)
if err != nil { if err != nil {
return server.JSON(w, http.StatusBadRequest, "Invalid page size") return server.Respond(w, http.StatusBadRequest, "Invalid page size")
} }
} }
@ -57,6 +55,6 @@ func (ctrl *V1Controller) HandleAssetGet() errchain.HandlerFunc {
log.Err(err).Msg("failed to get item") log.Err(err).Msg("failed to get item")
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusOK, items) return server.Respond(w, http.StatusOK, items)
} }
} }

View file

@ -3,23 +3,15 @@ package v1
import ( import (
"errors" "errors"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const (
cookieNameToken = "hb.auth.token"
cookieNameRemember = "hb.auth.remember"
cookieNameSession = "hb.auth.session"
)
type ( type (
TokenResponse struct { TokenResponse struct {
Token string `json:"token"` Token string `json:"token"`
@ -30,68 +22,39 @@ type (
LoginForm struct { LoginForm struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
StayLoggedIn bool `json:"stayLoggedIn"`
} }
) )
type CookieContents struct {
Token string
ExpiresAt time.Time
Remember bool
}
func GetCookies(r *http.Request) (*CookieContents, error) {
cookie, err := r.Cookie(cookieNameToken)
if err != nil {
return nil, errors.New("authorization cookie is required")
}
rememberCookie, err := r.Cookie(cookieNameRemember)
if err != nil {
return nil, errors.New("remember cookie is required")
}
return &CookieContents{
Token: cookie.Value,
ExpiresAt: cookie.Expires,
Remember: rememberCookie.Value == "true",
}, nil
}
// HandleAuthLogin godoc // HandleAuthLogin godoc
//
// @Summary User Login // @Summary User Login
// @Tags Authentication // @Tags Authentication
// @Accept x-www-form-urlencoded // @Accept x-www-form-urlencoded
// @Accept application/json // @Accept application/json
// @Param username formData string false "string" example(admin@admin.com) // @Param username formData string false "string" example(admin@admin.com)
// @Param password formData string false "string" example(admin) // @Param password formData string false "string" example(admin)
// @Param payload body LoginForm true "Login Data"
// @Produce json // @Produce json
// @Success 200 {object} TokenResponse // @Success 200 {object} TokenResponse
// @Router /v1/users/login [POST] // @Router /v1/users/login [POST]
func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc { func (ctrl *V1Controller) HandleAuthLogin() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
loginForm := &LoginForm{} loginForm := &LoginForm{}
switch r.Header.Get("Content-Type") { switch r.Header.Get("Content-Type") {
case "application/x-www-form-urlencoded": case server.ContentFormUrlEncoded:
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
return errors.New("failed to parse form") return server.Respond(w, http.StatusBadRequest, server.Wrap(err))
} }
loginForm.Username = r.PostFormValue("username") loginForm.Username = r.PostFormValue("username")
loginForm.Password = r.PostFormValue("password") loginForm.Password = r.PostFormValue("password")
loginForm.StayLoggedIn = r.PostFormValue("stayLoggedIn") == "true" case server.ContentJSON:
case "application/json":
err := server.Decode(r, loginForm) err := server.Decode(r, loginForm)
if err != nil { if err != nil {
log.Err(err).Msg("failed to decode login form") log.Err(err).Msg("failed to decode login form")
return errors.New("failed to decode login form")
} }
default: default:
return server.JSON(w, http.StatusBadRequest, errors.New("invalid content type")) return server.Respond(w, http.StatusBadRequest, errors.New("invalid content type"))
} }
if loginForm.Username == "" || loginForm.Password == "" { if loginForm.Username == "" || loginForm.Password == "" {
@ -107,13 +70,12 @@ func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc {
) )
} }
newToken, err := ctrl.svc.User.Login(r.Context(), strings.ToLower(loginForm.Username), loginForm.Password, loginForm.StayLoggedIn) newToken, err := ctrl.svc.User.Login(r.Context(), strings.ToLower(loginForm.Username), loginForm.Password)
if err != nil { if err != nil {
return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError) return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError)
} }
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, loginForm.StayLoggedIn) return server.Respond(w, http.StatusOK, TokenResponse{
return server.JSON(w, http.StatusOK, TokenResponse{
Token: "Bearer " + newToken.Raw, Token: "Bearer " + newToken.Raw,
ExpiresAt: newToken.ExpiresAt, ExpiresAt: newToken.ExpiresAt,
AttachmentToken: newToken.AttachmentToken, AttachmentToken: newToken.AttachmentToken,
@ -122,13 +84,12 @@ func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc {
} }
// HandleAuthLogout godoc // HandleAuthLogout godoc
//
// @Summary User Logout // @Summary User Logout
// @Tags Authentication // @Tags Authentication
// @Success 204 // @Success 204
// @Router /v1/users/logout [POST] // @Router /v1/users/logout [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc { func (ctrl *V1Controller) HandleAuthLogout() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
token := services.UseTokenCtx(r.Context()) token := services.UseTokenCtx(r.Context())
if token == "" { if token == "" {
@ -140,13 +101,11 @@ func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
ctrl.unsetCookies(w, noPort(r.Host)) return server.Respond(w, http.StatusNoContent, nil)
return server.JSON(w, http.StatusNoContent, nil)
} }
} }
// HandleAuthLogout godoc // HandleAuthLogout godoc
//
// @Summary User Token Refresh // @Summary User Token Refresh
// @Description handleAuthRefresh returns a handler that will issue a new token from an existing token. // @Description handleAuthRefresh returns a handler that will issue a new token from an existing token.
// @Description This does not validate that the user still exists within the database. // @Description This does not validate that the user still exists within the database.
@ -154,7 +113,7 @@ func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
// @Success 200 // @Success 200
// @Router /v1/users/refresh [GET] // @Router /v1/users/refresh [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc { func (ctrl *V1Controller) HandleAuthRefresh() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
requestToken := services.UseTokenCtx(r.Context()) requestToken := services.UseTokenCtx(r.Context())
if requestToken == "" { if requestToken == "" {
@ -166,78 +125,6 @@ func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
return validate.NewUnauthorizedError() return validate.NewUnauthorizedError()
} }
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, false) return server.Respond(w, http.StatusOK, newToken)
return server.JSON(w, http.StatusOK, newToken)
} }
} }
func noPort(host string) string {
return strings.Split(host, ":")[0]
}
func (ctrl *V1Controller) setCookies(w http.ResponseWriter, domain, token string, expires time.Time, remember bool) {
http.SetCookie(w, &http.Cookie{
Name: cookieNameRemember,
Value: strconv.FormatBool(remember),
Expires: expires,
Domain: domain,
Secure: ctrl.cookieSecure,
HttpOnly: true,
Path: "/",
})
// Set HTTP only cookie
http.SetCookie(w, &http.Cookie{
Name: cookieNameToken,
Value: token,
Expires: expires,
Domain: domain,
Secure: ctrl.cookieSecure,
HttpOnly: true,
Path: "/",
})
// Set Fake Session cookie
http.SetCookie(w, &http.Cookie{
Name: cookieNameSession,
Value: "true",
Expires: expires,
Domain: domain,
Secure: ctrl.cookieSecure,
HttpOnly: false,
Path: "/",
})
}
func (ctrl *V1Controller) unsetCookies(w http.ResponseWriter, domain string) {
http.SetCookie(w, &http.Cookie{
Name: cookieNameToken,
Value: "",
Expires: time.Unix(0, 0),
Domain: domain,
Secure: ctrl.cookieSecure,
HttpOnly: true,
Path: "/",
})
http.SetCookie(w, &http.Cookie{
Name: cookieNameRemember,
Value: "false",
Expires: time.Unix(0, 0),
Domain: domain,
Secure: ctrl.cookieSecure,
HttpOnly: true,
Path: "/",
})
// Set Fake Session cookie
http.SetCookie(w, &http.Cookie{
Name: cookieNameSession,
Value: "false",
Expires: time.Unix(0, 0),
Domain: domain,
Secure: ctrl.cookieSecure,
HttpOnly: false,
Path: "/",
})
}

View file

@ -6,13 +6,14 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "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/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
) )
type ( type (
GroupInvitationCreate struct { GroupInvitationCreate struct {
Uses int `json:"uses" validate:"required,min=1,max=100"` Uses int `json:"uses"`
ExpiresAt time.Time `json:"expiresAt"` ExpiresAt time.Time `json:"expiresAt"`
} }
@ -24,65 +25,93 @@ type (
) )
// HandleGroupGet godoc // HandleGroupGet godoc
// // @Summary Get the current user's group
// @Summary Get Group
// @Tags Group // @Tags Group
// @Produce json // @Produce json
// @Success 200 {object} repo.Group // @Success 200 {object} repo.Group
// @Router /v1/groups [Get] // @Router /v1/groups [Get]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupGet() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGroupGet() server.HandlerFunc {
fn := func(r *http.Request) (repo.Group, error) { return ctrl.handleGroupGeneral()
auth := services.NewContext(r.Context())
return ctrl.repo.Groups.GroupByID(auth, auth.GID)
}
return adapters.Command(fn, http.StatusOK)
} }
// HandleGroupUpdate godoc // HandleGroupUpdate godoc
// // @Summary Updates some fields of the current users group
// @Summary Update Group
// @Tags Group // @Tags Group
// @Produce json // @Produce json
// @Param payload body repo.GroupUpdate true "User Data" // @Param payload body repo.GroupUpdate true "User Data"
// @Success 200 {object} repo.Group // @Success 200 {object} repo.Group
// @Router /v1/groups [Put] // @Router /v1/groups [Put]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupUpdate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGroupUpdate() server.HandlerFunc {
fn := func(r *http.Request, body repo.GroupUpdate) (repo.Group, error) { return ctrl.handleGroupGeneral()
auth := services.NewContext(r.Context()) }
return ctrl.svc.Group.UpdateGroup(auth, body)
func (ctrl *V1Controller) handleGroupGeneral() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
switch r.Method {
case http.MethodGet:
group, err := ctrl.repo.Groups.GroupByID(ctx, ctx.GID)
if err != nil {
log.Err(err).Msg("failed to get group")
return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return adapters.Action(fn, http.StatusOK) return server.Respond(w, http.StatusOK, group)
case http.MethodPut:
data := repo.GroupUpdate{}
if err := server.Decode(r, &data); err != nil {
return validate.NewRequestError(err, http.StatusBadRequest)
}
group, err := ctrl.svc.Group.UpdateGroup(ctx, data)
if err != nil {
log.Err(err).Msg("failed to update group")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, group)
}
return nil
}
} }
// HandleGroupInvitationsCreate godoc // HandleGroupInvitationsCreate godoc
// // @Summary Get the current user
// @Summary Create Group Invitation
// @Tags Group // @Tags Group
// @Produce json // @Produce json
// @Param payload body GroupInvitationCreate true "User Data" // @Param payload body GroupInvitationCreate true "User Data"
// @Success 200 {object} GroupInvitation // @Success 200 {object} GroupInvitation
// @Router /v1/groups/invitations [Post] // @Router /v1/groups/invitations [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupInvitationsCreate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGroupInvitationsCreate() server.HandlerFunc {
fn := func(r *http.Request, body GroupInvitationCreate) (GroupInvitation, error) { return func(w http.ResponseWriter, r *http.Request) error {
if body.ExpiresAt.IsZero() { data := GroupInvitationCreate{}
body.ExpiresAt = time.Now().Add(time.Hour * 24) if err := server.Decode(r, &data); err != nil {
log.Err(err).Msg("failed to decode user registration data")
return validate.NewRequestError(err, http.StatusBadRequest)
} }
auth := services.NewContext(r.Context()) if data.ExpiresAt.IsZero() {
data.ExpiresAt = time.Now().Add(time.Hour * 24)
}
token, err := ctrl.svc.Group.NewInvitation(auth, body.Uses, body.ExpiresAt) ctx := services.NewContext(r.Context())
return GroupInvitation{ token, err := ctrl.svc.Group.NewInvitation(ctx, data.Uses, data.ExpiresAt)
if err != nil {
log.Err(err).Msg("failed to create new token")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, GroupInvitation{
Token: token, Token: token,
ExpiresAt: body.ExpiresAt, ExpiresAt: data.ExpiresAt,
Uses: body.Uses, Uses: data.Uses,
}, err })
} }
return adapters.Action(fn, http.StatusCreated)
} }

View file

@ -2,24 +2,19 @@ package v1
import ( import (
"database/sql" "database/sql"
"encoding/csv"
"errors" "errors"
"net/http" "net/http"
"strings" "strings"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "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/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/errchain"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// HandleItemsGetAll godoc // HandleItemsGetAll godoc
// // @Summary Get All Items
// @Summary Query All Items
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Param q query string false "search string" // @Param q query string false "search string"
@ -30,7 +25,7 @@ import (
// @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{} // @Success 200 {object} repo.PaginationResult[repo.ItemSummary]{}
// @Router /v1/items [GET] // @Router /v1/items [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
extractQuery := func(r *http.Request) repo.ItemQuery { extractQuery := func(r *http.Request) repo.ItemQuery {
params := r.URL.Query() params := r.URL.Query()
@ -58,7 +53,6 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
LabelIDs: queryUUIDList(params, "labels"), LabelIDs: queryUUIDList(params, "labels"),
IncludeArchived: queryBool(params.Get("includeArchived")), IncludeArchived: queryBool(params.Get("includeArchived")),
Fields: filterFieldItems(params["fields"]), Fields: filterFieldItems(params["fields"]),
OrderBy: params.Get("orderBy"),
} }
if strings.HasPrefix(v.Search, "#") { if strings.HasPrefix(v.Search, "#") {
@ -80,75 +74,70 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r)) items, err := ctrl.repo.Items.QueryByGroup(ctx, ctx.GID, extractQuery(r))
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return server.JSON(w, http.StatusOK, repo.PaginationResult[repo.ItemSummary]{ return server.Respond(w, http.StatusOK, repo.PaginationResult[repo.ItemSummary]{
Items: []repo.ItemSummary{}, Items: []repo.ItemSummary{},
}) })
} }
log.Err(err).Msg("failed to get items") log.Err(err).Msg("failed to get items")
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusOK, items) return server.Respond(w, http.StatusOK, items)
} }
} }
// HandleItemsCreate godoc // HandleItemsCreate godoc
// // @Summary Create a new item
// @Summary Create Item
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Param payload body repo.ItemCreate true "Item Data" // @Param payload body repo.ItemCreate true "Item Data"
// @Success 201 {object} repo.ItemSummary // @Success 200 {object} repo.ItemSummary
// @Router /v1/items [POST] // @Router /v1/items [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemsCreate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemsCreate() server.HandlerFunc {
fn := func(r *http.Request, body repo.ItemCreate) (repo.ItemOut, error) { return func(w http.ResponseWriter, r *http.Request) error {
return ctrl.svc.Items.Create(services.NewContext(r.Context()), body) createData := repo.ItemCreate{}
if err := server.Decode(r, &createData); err != nil {
log.Err(err).Msg("failed to decode request body")
return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return adapters.Action(fn, http.StatusCreated) ctx := services.NewContext(r.Context())
item, err := ctrl.svc.Items.Create(ctx, createData)
if err != nil {
log.Err(err).Msg("failed to create item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, item)
}
} }
// HandleItemGet godocs // HandleItemGet godocs
// // @Summary Gets a item and fields
// @Summary Get Item
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Param id path string true "Item ID" // @Param id path string true "Item ID"
// @Success 200 {object} repo.ItemOut // @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [GET] // @Router /v1/items/{id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemGet() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemGet() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.ItemOut, error) { return ctrl.handleItemsGeneral()
auth := services.NewContext(r.Context())
return ctrl.repo.Items.GetOneByGroup(auth, auth.GID, ID)
}
return adapters.CommandID("id", fn, http.StatusOK)
} }
// HandleItemDelete godocs // HandleItemDelete godocs
// // @Summary deletes a item
// @Summary Delete Item
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Param id path string true "Item ID" // @Param id path string true "Item ID"
// @Success 204 // @Success 204
// @Router /v1/items/{id} [DELETE] // @Router /v1/items/{id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemDelete() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) { return ctrl.handleItemsGeneral()
auth := services.NewContext(r.Context())
err := ctrl.repo.Items.DeleteByGroup(auth, auth.GID, ID)
return nil, err
}
return adapters.CommandID("id", fn, http.StatusNoContent)
} }
// HandleItemUpdate godocs // HandleItemUpdate godocs
// // @Summary updates a item
// @Summary Update Item
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Param id path string true "Item ID" // @Param id path string true "Item ID"
@ -156,95 +145,103 @@ func (ctrl *V1Controller) HandleItemDelete() errchain.HandlerFunc {
// @Success 200 {object} repo.ItemOut // @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [PUT] // @Router /v1/items/{id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemUpdate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemUpdate() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemUpdate) (repo.ItemOut, error) { return ctrl.handleItemsGeneral()
auth := services.NewContext(r.Context())
body.ID = ID
return ctrl.repo.Items.UpdateByGroup(auth, auth.GID, body)
}
return adapters.ActionID("id", fn, http.StatusOK)
} }
func (ctrl *V1Controller) handleItemsGeneral() server.HandlerFunc {
// HandleItemPatch godocs return func(w http.ResponseWriter, r *http.Request) error {
// ctx := services.NewContext(r.Context())
// @Summary Update Item ID, err := ctrl.routeID(r)
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Param payload body repo.ItemPatch true "Item Data"
// @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id} [Patch]
// @Security Bearer
func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemPatch) (repo.ItemOut, error) {
auth := services.NewContext(r.Context())
body.ID = ID
err := ctrl.repo.Items.Patch(auth, auth.GID, ID, body)
if err != nil { if err != nil {
return repo.ItemOut{}, err return err
} }
return ctrl.repo.Items.GetOneByGroup(auth, auth.GID, ID) switch r.Method {
case http.MethodGet:
items, err := ctrl.repo.Items.GetOneByGroup(r.Context(), ctx.GID, ID)
if err != nil {
log.Err(err).Msg("failed to get item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, items)
case http.MethodDelete:
err = ctrl.repo.Items.DeleteByGroup(r.Context(), ctx.GID, ID)
if err != nil {
log.Err(err).Msg("failed to delete item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
case http.MethodPut:
body := repo.ItemUpdate{}
if err := server.Decode(r, &body); err != nil {
log.Err(err).Msg("failed to decode request body")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
body.ID = ID
result, err := ctrl.repo.Items.UpdateByGroup(r.Context(), ctx.GID, body)
if err != nil {
log.Err(err).Msg("failed to update item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, result)
} }
return adapters.ActionID("id", fn, http.StatusOK) return nil
}
} }
// HandleGetAllCustomFieldNames godocs // HandleGetAllCustomFieldNames godocs
// // @Summary imports items into the database
// @Summary Get All Custom Field Names
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Success 200 // @Success 200
// @Router /v1/items/fields [GET] // @Router /v1/items/fields [GET]
// @Success 200 {object} []string // @Success 200 {object} []string
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGetAllCustomFieldNames() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGetAllCustomFieldNames() server.HandlerFunc {
fn := func(r *http.Request) ([]string, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) ctx := services.NewContext(r.Context())
return ctrl.repo.Items.GetAllCustomFieldNames(auth, auth.GID)
v, err := ctrl.repo.Items.GetAllCustomFieldNames(r.Context(), ctx.GID)
if err != nil {
return err
} }
return adapters.Command(fn, http.StatusOK) return server.Respond(w, http.StatusOK, v)
}
} }
// HandleGetAllCustomFieldValues godocs // HandleGetAllCustomFieldValues godocs
// // @Summary imports items into the database
// @Summary Get All Custom Field Values
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Success 200 // @Success 200
// @Router /v1/items/fields/values [GET] // @Router /v1/items/fields/values [GET]
// @Success 200 {object} []string // @Success 200 {object} []string
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGetAllCustomFieldValues() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGetAllCustomFieldValues() server.HandlerFunc {
type query struct { return func(w http.ResponseWriter, r *http.Request) error {
Field string `schema:"field" validate:"required"` ctx := services.NewContext(r.Context())
v, err := ctrl.repo.Items.GetAllCustomFieldValues(r.Context(), ctx.GID, r.URL.Query().Get("field"))
if err != nil {
return err
} }
fn := func(r *http.Request, q query) ([]string, error) { return server.Respond(w, http.StatusOK, v)
auth := services.NewContext(r.Context())
return ctrl.repo.Items.GetAllCustomFieldValues(auth, auth.GID, q.Field)
} }
return adapters.Query(fn, http.StatusOK)
} }
// HandleItemsImport godocs // HandleItemsImport godocs
// // @Summary imports items into the database
// @Summary Import Items
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Success 204 // @Success 204
// @Param csv formData file true "Image to upload" // @Param csv formData file true "Image to upload"
// @Router /v1/items/import [Post] // @Router /v1/items/import [Post]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemsImport() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20) err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil { if err != nil {
@ -258,40 +255,20 @@ func (ctrl *V1Controller) HandleItemsImport() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
data, err := services.ReadCsv(file)
if err != nil {
log.Err(err).Msg("failed to read csv")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
user := services.UseUserCtx(r.Context()) user := services.UseUserCtx(r.Context())
_, err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, file) _, err = ctrl.svc.Items.CsvImport(r.Context(), user.GroupID, data)
if err != nil { if err != nil {
log.Err(err).Msg("failed to import items") log.Err(err).Msg("failed to import items")
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusNoContent, nil) return server.Respond(w, http.StatusNoContent, nil)
}
}
// HandleItemsExport godocs
//
// @Summary Export Items
// @Tags Items
// @Success 200 {string} string "text/csv"
// @Router /v1/items/export [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemsExport() errchain.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
csvData, err := ctrl.svc.Items.ExportTSV(r.Context(), ctx.GID)
if err != nil {
log.Err(err).Msg("failed to export items")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "text/tsv")
w.Header().Set("Content-Disposition", "attachment;filename=homebox-items.tsv")
writer := csv.NewWriter(w)
writer.Comma = '\t'
return writer.WriteAll(csvData)
} }
} }

View file

@ -8,8 +8,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment" "github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -19,9 +18,8 @@ type (
} }
) )
// HandleItemAttachmentCreate godocs // HandleItemsImport godocs
// // @Summary imports items into the database
// @Summary Create Item Attachment
// @Tags Items Attachments // @Tags Items Attachments
// @Produce json // @Produce json
// @Param id path string true "Item ID" // @Param id path string true "Item ID"
@ -29,10 +27,10 @@ type (
// @Param type formData string true "Type of file" // @Param type formData string true "Type of file"
// @Param name formData string true "name of the file including extension" // @Param name formData string true "name of the file including extension"
// @Success 200 {object} repo.ItemOut // @Success 200 {object} repo.ItemOut
// @Failure 422 {object} validate.ErrorResponse // @Failure 422 {object} server.ErrorResponse
// @Router /v1/items/{id}/attachments [POST] // @Router /v1/items/{id}/attachments [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemAttachmentCreate() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20) err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
if err != nil { if err != nil {
@ -62,7 +60,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
} }
if !errs.Nil() { if !errs.Nil() {
return server.JSON(w, http.StatusUnprocessableEntity, errs) return server.Respond(w, http.StatusUnprocessableEntity, errs)
} }
attachmentType := r.FormValue("type") attachmentType := r.FormValue("type")
@ -89,13 +87,12 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusCreated, item) return server.Respond(w, http.StatusCreated, item)
} }
} }
// HandleItemAttachmentGet godocs // HandleItemAttachmentGet godocs
// // @Summary retrieves an attachment for an item
// @Summary Get Item Attachment
// @Tags Items Attachments // @Tags Items Attachments
// @Produce application/octet-stream // @Produce application/octet-stream
// @Param id path string true "Item ID" // @Param id path string true "Item ID"
@ -103,26 +100,24 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
// @Success 200 {object} ItemAttachmentToken // @Success 200 {object} ItemAttachmentToken
// @Router /v1/items/{id}/attachments/{attachment_id} [GET] // @Router /v1/items/{id}/attachments/{attachment_id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentGet() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemAttachmentGet() server.HandlerFunc {
return ctrl.handleItemAttachmentsHandler return ctrl.handleItemAttachmentsHandler
} }
// HandleItemAttachmentDelete godocs // HandleItemAttachmentDelete godocs
// // @Summary retrieves an attachment for an item
// @Summary Delete Item Attachment
// @Tags Items Attachments // @Tags Items Attachments
// @Param id path string true "Item ID" // @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID" // @Param attachment_id path string true "Attachment ID"
// @Success 204 // @Success 204
// @Router /v1/items/{id}/attachments/{attachment_id} [DELETE] // @Router /v1/items/{id}/attachments/{attachment_id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentDelete() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemAttachmentDelete() server.HandlerFunc {
return ctrl.handleItemAttachmentsHandler return ctrl.handleItemAttachmentsHandler
} }
// HandleItemAttachmentUpdate godocs // HandleItemAttachmentUpdate godocs
// // @Summary retrieves an attachment for an item
// @Summary Update Item Attachment
// @Tags Items Attachments // @Tags Items Attachments
// @Param id path string true "Item ID" // @Param id path string true "Item ID"
// @Param attachment_id path string true "Attachment ID" // @Param attachment_id path string true "Attachment ID"
@ -130,7 +125,7 @@ func (ctrl *V1Controller) HandleItemAttachmentDelete() errchain.HandlerFunc {
// @Success 200 {object} repo.ItemOut // @Success 200 {object} repo.ItemOut
// @Router /v1/items/{id}/attachments/{attachment_id} [PUT] // @Router /v1/items/{id}/attachments/{attachment_id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemAttachmentUpdate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemAttachmentUpdate() server.HandlerFunc {
return ctrl.handleItemAttachmentsHandler return ctrl.handleItemAttachmentsHandler
} }
@ -165,7 +160,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusNoContent, nil) return server.Respond(w, http.StatusNoContent, nil)
// Update Attachment Handler // Update Attachment Handler
case http.MethodPut: case http.MethodPut:
@ -183,7 +178,7 @@ func (ctrl *V1Controller) handleItemAttachmentsHandler(w http.ResponseWriter, r
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusOK, val) return server.Respond(w, http.StatusOK, val)
} }
return nil return nil

View file

@ -3,100 +3,141 @@ package v1
import ( import (
"net/http" "net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
) )
// HandleLabelsGetAll godoc // HandleLabelsGetAll godoc
//
// @Summary Get All Labels // @Summary Get All Labels
// @Tags Labels // @Tags Labels
// @Produce json // @Produce json
// @Success 200 {object} []repo.LabelOut // @Success 200 {object} server.Results{items=[]repo.LabelOut}
// @Router /v1/labels [GET] // @Router /v1/labels [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLabelsGetAll() server.HandlerFunc {
fn := func(r *http.Request) ([]repo.LabelSummary, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) user := services.UseUserCtx(r.Context())
return ctrl.repo.Labels.GetAll(auth, auth.GID) labels, err := ctrl.repo.Labels.GetAll(r.Context(), user.GroupID)
if err != nil {
log.Err(err).Msg("error getting labels")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, server.Results{Items: labels})
} }
return adapters.Command(fn, http.StatusOK)
} }
// HandleLabelsCreate godoc // HandleLabelsCreate godoc
// // @Summary Create a new label
// @Summary Create Label
// @Tags Labels // @Tags Labels
// @Produce json // @Produce json
// @Param payload body repo.LabelCreate true "Label Data" // @Param payload body repo.LabelCreate true "Label Data"
// @Success 200 {object} repo.LabelSummary // @Success 200 {object} repo.LabelSummary
// @Router /v1/labels [POST] // @Router /v1/labels [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelsCreate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLabelsCreate() server.HandlerFunc {
fn := func(r *http.Request, data repo.LabelCreate) (repo.LabelOut, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) createData := repo.LabelCreate{}
return ctrl.repo.Labels.Create(auth, auth.GID, data) if err := server.Decode(r, &createData); err != nil {
log.Err(err).Msg("error decoding label create data")
return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return adapters.Action(fn, http.StatusCreated) user := services.UseUserCtx(r.Context())
label, err := ctrl.repo.Labels.Create(r.Context(), user.GroupID, createData)
if err != nil {
log.Err(err).Msg("error creating label")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, label)
}
} }
// HandleLabelDelete godocs // HandleLabelDelete godocs
// // @Summary deletes a label
// @Summary Delete Label
// @Tags Labels // @Tags Labels
// @Produce json // @Produce json
// @Param id path string true "Label ID" // @Param id path string true "Label ID"
// @Success 204 // @Success 204
// @Router /v1/labels/{id} [DELETE] // @Router /v1/labels/{id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelDelete() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLabelDelete() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) { return ctrl.handleLabelsGeneral()
auth := services.NewContext(r.Context())
err := ctrl.repo.Labels.DeleteByGroup(auth, auth.GID, ID)
return nil, err
}
return adapters.CommandID("id", fn, http.StatusNoContent)
} }
// HandleLabelGet godocs // HandleLabelGet godocs
// // @Summary Gets a label and fields
// @Summary Get Label
// @Tags Labels // @Tags Labels
// @Produce json // @Produce json
// @Param id path string true "Label ID" // @Param id path string true "Label ID"
// @Success 200 {object} repo.LabelOut // @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [GET] // @Router /v1/labels/{id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelGet() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLabelGet() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.LabelOut, error) { return ctrl.handleLabelsGeneral()
auth := services.NewContext(r.Context())
return ctrl.repo.Labels.GetOneByGroup(auth, auth.GID, ID)
}
return adapters.CommandID("id", fn, http.StatusOK)
} }
// HandleLabelUpdate godocs // HandleLabelUpdate godocs
// // @Summary updates a label
// @Summary Update Label
// @Tags Labels // @Tags Labels
// @Produce json // @Produce json
// @Param id path string true "Label ID" // @Param id path string true "Label ID"
// @Success 200 {object} repo.LabelOut // @Success 200 {object} repo.LabelOut
// @Router /v1/labels/{id} [PUT] // @Router /v1/labels/{id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelUpdate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLabelUpdate() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, data repo.LabelUpdate) (repo.LabelOut, error) { return ctrl.handleLabelsGeneral()
auth := services.NewContext(r.Context()) }
data.ID = ID
return ctrl.repo.Labels.UpdateByGroup(auth, auth.GID, data) func (ctrl *V1Controller) handleLabelsGeneral() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
ID, err := ctrl.routeID(r)
if err != nil {
return err
} }
return adapters.ActionID("id", fn, http.StatusOK) switch r.Method {
case http.MethodGet:
labels, err := ctrl.repo.Labels.GetOneByGroup(r.Context(), ctx.GID, ID)
if err != nil {
if ent.IsNotFound(err) {
log.Err(err).
Str("id", ID.String()).
Msg("label not found")
return validate.NewRequestError(err, http.StatusNotFound)
}
log.Err(err).Msg("error getting label")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, labels)
case http.MethodDelete:
err = ctrl.repo.Labels.DeleteByGroup(ctx, ctx.GID, ID)
if err != nil {
log.Err(err).Msg("error deleting label")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
case http.MethodPut:
body := repo.LabelUpdate{}
if err := server.Decode(r, &body); err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
body.ID = ID
result, err := ctrl.repo.Labels.UpdateByGroup(ctx, ctx.GID, body)
if err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, result)
}
return nil
}
} }

View file

@ -3,107 +3,127 @@ package v1
import ( import (
"net/http" "net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
) )
// HandleLocationTreeQuery // HandleLocationTreeQuery godoc
// // @Summary Get All Locations
// @Summary Get Locations Tree
// @Tags Locations // @Tags Locations
// @Produce json // @Produce json
// @Param withItems query bool false "include items in response tree" // @Param withItems query bool false "include items in response tree"
// @Success 200 {object} []repo.TreeItem // @Success 200 {object} server.Results{items=[]repo.TreeItem}
// @Router /v1/locations/tree [GET] // @Router /v1/locations/tree [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLocationTreeQuery() server.HandlerFunc {
fn := func(r *http.Request, query repo.TreeQuery) ([]repo.TreeItem, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) user := services.UseUserCtx(r.Context())
return ctrl.repo.Locations.Tree(auth, auth.GID, query)
q := r.URL.Query()
withItems := queryBool(q.Get("withItems"))
locTree, err := ctrl.repo.Locations.Tree(
r.Context(),
user.GroupID,
repo.TreeQuery{
WithItems: withItems,
},
)
if err != nil {
log.Err(err).Msg("failed to get locations tree")
return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return adapters.Query(fn, http.StatusOK) return server.Respond(w, http.StatusOK, server.Results{Items: locTree})
}
} }
// HandleLocationGetAll // HandleLocationGetAll godoc
//
// @Summary Get All Locations // @Summary Get All Locations
// @Tags Locations // @Tags Locations
// @Produce json // @Produce json
// @Param filterChildren query bool false "Filter locations with parents" // @Param filterChildren query bool false "Filter locations with parents"
// @Success 200 {object} []repo.LocationOutCount // @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
// @Router /v1/locations [GET] // @Router /v1/locations [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
fn := func(r *http.Request, q repo.LocationQuery) ([]repo.LocationOutCount, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) user := services.UseUserCtx(r.Context())
return ctrl.repo.Locations.GetAll(auth, auth.GID, q)
q := r.URL.Query()
filter := repo.LocationQuery{
FilterChildren: queryBool(q.Get("filterChildren")),
} }
return adapters.Query(fn, http.StatusOK) locations, err := ctrl.repo.Locations.GetAll(r.Context(), user.GroupID, filter)
if err != nil {
log.Err(err).Msg("failed to get locations")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, server.Results{Items: locations})
}
} }
// HandleLocationCreate // HandleLocationCreate godoc
// // @Summary Create a new location
// @Summary Create Location
// @Tags Locations // @Tags Locations
// @Produce json // @Produce json
// @Param payload body repo.LocationCreate true "Location Data" // @Param payload body repo.LocationCreate true "Location Data"
// @Success 200 {object} repo.LocationSummary // @Success 200 {object} repo.LocationSummary
// @Router /v1/locations [POST] // @Router /v1/locations [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationCreate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLocationCreate() server.HandlerFunc {
fn := func(r *http.Request, createData repo.LocationCreate) (repo.LocationOut, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) createData := repo.LocationCreate{}
return ctrl.repo.Locations.Create(auth, auth.GID, createData) if err := server.Decode(r, &createData); err != nil {
log.Err(err).Msg("failed to decode location create data")
return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return adapters.Action(fn, http.StatusCreated) user := services.UseUserCtx(r.Context())
location, err := ctrl.repo.Locations.Create(r.Context(), user.GroupID, createData)
if err != nil {
log.Err(err).Msg("failed to create location")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, location)
}
} }
// HandleLocationDelete // HandleLocationDelete godocs
// // @Summary deletes a location
// @Summary Delete Location
// @Tags Locations // @Tags Locations
// @Produce json // @Produce json
// @Param id path string true "Location ID" // @Param id path string true "Location ID"
// @Success 204 // @Success 204
// @Router /v1/locations/{id} [DELETE] // @Router /v1/locations/{id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationDelete() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLocationDelete() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) { return ctrl.handleLocationGeneral()
auth := services.NewContext(r.Context())
err := ctrl.repo.Locations.DeleteByGroup(auth, auth.GID, ID)
return nil, err
}
return adapters.CommandID("id", fn, http.StatusNoContent)
} }
// HandleLocationGet // HandleLocationGet godocs
// // @Summary Gets a location and fields
// @Summary Get Location
// @Tags Locations // @Tags Locations
// @Produce json // @Produce json
// @Param id path string true "Location ID" // @Param id path string true "Location ID"
// @Success 200 {object} repo.LocationOut // @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [GET] // @Router /v1/locations/{id} [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLocationGet() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (repo.LocationOut, error) { return ctrl.handleLocationGeneral()
auth := services.NewContext(r.Context())
return ctrl.repo.Locations.GetOneByGroup(auth, auth.GID, ID)
}
return adapters.CommandID("id", fn, http.StatusOK)
} }
// HandleLocationUpdate // HandleLocationUpdate godocs
// // @Summary updates a location
// @Summary Update Location
// @Tags Locations // @Tags Locations
// @Produce json // @Produce json
// @Param id path string true "Location ID" // @Param id path string true "Location ID"
@ -111,12 +131,58 @@ func (ctrl *V1Controller) HandleLocationGet() errchain.HandlerFunc {
// @Success 200 {object} repo.LocationOut // @Success 200 {object} repo.LocationOut
// @Router /v1/locations/{id} [PUT] // @Router /v1/locations/{id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLocationUpdate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleLocationUpdate() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, body repo.LocationUpdate) (repo.LocationOut, error) { return ctrl.handleLocationGeneral()
auth := services.NewContext(r.Context()) }
body.ID = ID
return ctrl.repo.Locations.UpdateByGroup(auth, auth.GID, ID, body) func (ctrl *V1Controller) handleLocationGeneral() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
ID, err := ctrl.routeID(r)
if err != nil {
return err
} }
return adapters.ActionID("id", fn, http.StatusOK) switch r.Method {
case http.MethodGet:
location, err := ctrl.repo.Locations.GetOneByGroup(r.Context(), ctx.GID, ID)
if err != nil {
l := log.Err(err).
Str("ID", ID.String()).
Str("GID", ctx.GID.String())
if ent.IsNotFound(err) {
l.Msg("location not found")
return validate.NewRequestError(err, http.StatusNotFound)
}
l.Msg("failed to get location")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, location)
case http.MethodPut:
body := repo.LocationUpdate{}
if err := server.Decode(r, &body); err != nil {
log.Err(err).Msg("failed to decode location update data")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
body.ID = ID
result, err := ctrl.repo.Locations.UpdateOneByGroup(r.Context(), ctx.GID, ID, body)
if err != nil {
log.Err(err).Msg("failed to update location")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, result)
case http.MethodDelete:
err = ctrl.repo.Locations.DeleteByGroup(r.Context(), ctx.GID, ID)
if err != nil {
log.Err(err).Msg("failed to delete location")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
}
return nil
}
} }

View file

@ -3,68 +3,48 @@ package v1
import ( import (
"net/http" "net/http"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services" "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/data/repo"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/rs/zerolog/log"
) )
// HandleMaintenanceGetLog godoc // HandleMaintenanceGetLog godoc
//
// @Summary Get Maintenance Log // @Summary Get Maintenance Log
// @Tags Maintenance // @Tags Maintenance
// @Produce json // @Produce json
// @Success 200 {object} repo.MaintenanceLog // @Success 200 {object} repo.MaintenanceLog
// @Router /v1/items/{id}/maintenance [GET] // @Router /v1/items/{id}/maintenance [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceLogGet() errchain.HandlerFunc { func (ctrl *V1Controller) HandleMaintenanceLogGet() server.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, q repo.MaintenanceLogQuery) (repo.MaintenanceLog, error) { return ctrl.handleMaintenanceLog()
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.GetLog(auth, auth.GID, ID, q)
}
return adapters.QueryID("id", fn, http.StatusOK)
} }
// HandleMaintenanceEntryCreate godoc // HandleMaintenanceEntryCreate godoc
//
// @Summary Create Maintenance Entry // @Summary Create Maintenance Entry
// @Tags Maintenance // @Tags Maintenance
// @Produce json // @Produce json
// @Param payload body repo.MaintenanceEntryCreate true "Entry Data" // @Param payload body repo.MaintenanceEntryCreate true "Entry Data"
// @Success 201 {object} repo.MaintenanceEntry // @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance [POST] // @Router /v1/items/{id}/maintenance [POST]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryCreate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleMaintenanceEntryCreate() server.HandlerFunc {
fn := func(r *http.Request, itemID uuid.UUID, body repo.MaintenanceEntryCreate) (repo.MaintenanceEntry, error) { return ctrl.handleMaintenanceLog()
auth := services.NewContext(r.Context())
return ctrl.repo.MaintEntry.Create(auth, itemID, body)
}
return adapters.ActionID("id", fn, http.StatusCreated)
} }
// HandleMaintenanceEntryDelete godoc // HandleMaintenanceEntryDelete godoc
//
// @Summary Delete Maintenance Entry // @Summary Delete Maintenance Entry
// @Tags Maintenance // @Tags Maintenance
// @Produce json // @Produce json
// @Success 204 // @Success 204
// @Router /v1/items/{id}/maintenance/{entry_id} [DELETE] // @Router /v1/items/{id}/maintenance/{entry_id} [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc { func (ctrl *V1Controller) HandleMaintenanceEntryDelete() server.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID) (any, error) { return ctrl.handleMaintenanceLog()
auth := services.NewContext(r.Context())
err := ctrl.repo.MaintEntry.Delete(auth, entryID)
return nil, err
}
return adapters.CommandID("entry_id", fn, http.StatusNoContent)
} }
// HandleMaintenanceEntryUpdate godoc // HandleMaintenanceEntryUpdate godoc
//
// @Summary Update Maintenance Entry // @Summary Update Maintenance Entry
// @Tags Maintenance // @Tags Maintenance
// @Produce json // @Produce json
@ -72,11 +52,74 @@ func (ctrl *V1Controller) HandleMaintenanceEntryDelete() errchain.HandlerFunc {
// @Success 200 {object} repo.MaintenanceEntry // @Success 200 {object} repo.MaintenanceEntry
// @Router /v1/items/{id}/maintenance/{entry_id} [PUT] // @Router /v1/items/{id}/maintenance/{entry_id} [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleMaintenanceEntryUpdate() server.HandlerFunc {
fn := func(r *http.Request, entryID uuid.UUID, body repo.MaintenanceEntryUpdate) (repo.MaintenanceEntry, error) { return ctrl.handleMaintenanceLog()
auth := services.NewContext(r.Context()) }
return ctrl.repo.MaintEntry.Update(auth, entryID, body)
func (ctrl *V1Controller) handleMaintenanceLog() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := services.NewContext(r.Context())
itemID, err := ctrl.routeID(r)
if err != nil {
return err
} }
return adapters.ActionID("entry_id", fn, http.StatusOK) switch r.Method {
case http.MethodGet:
mlog, err := ctrl.repo.MaintEntry.GetLog(ctx, itemID)
if err != nil {
log.Err(err).Msg("failed to get items")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, mlog)
case http.MethodPost:
var create repo.MaintenanceEntryCreate
err := server.Decode(r, &create)
if err != nil {
return validate.NewRequestError(err, http.StatusBadRequest)
}
entry, err := ctrl.repo.MaintEntry.Create(ctx, itemID, create)
if err != nil {
log.Err(err).Msg("failed to create item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusCreated, entry)
case http.MethodPut:
entryID, err := ctrl.routeUUID(r, "entry_id")
if err != nil {
return err
}
var update repo.MaintenanceEntryUpdate
err = server.Decode(r, &update)
if err != nil {
return validate.NewRequestError(err, http.StatusBadRequest)
}
entry, err := ctrl.repo.MaintEntry.Update(ctx, entryID, update)
if err != nil {
log.Err(err).Msg("failed to update item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusOK, entry)
case http.MethodDelete:
entryID, err := ctrl.routeUUID(r, "entry_id")
if err != nil {
return err
}
err = ctrl.repo.MaintEntry.Delete(ctx, entryID)
if err != nil {
log.Err(err).Msg("failed to delete item")
return validate.NewRequestError(err, http.StatusInternalServerError)
}
return server.Respond(w, http.StatusNoContent, nil)
}
return nil
}
} }

View file

@ -1,105 +0,0 @@
package v1
import (
"net/http"
"github.com/containrrr/shoutrrr"
"github.com/google/uuid"
"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/web/adapters"
"github.com/hay-kot/httpkit/errchain"
)
// HandleGetUserNotifiers godoc
//
// @Summary Get Notifiers
// @Tags Notifiers
// @Produce json
// @Success 200 {object} []repo.NotifierOut
// @Router /v1/notifiers [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc {
fn := func(r *http.Request, _ struct{}) ([]repo.NotifierOut, error) {
user := services.UseUserCtx(r.Context())
return ctrl.repo.Notifiers.GetByUser(r.Context(), user.ID)
}
return adapters.Query(fn, http.StatusOK)
}
// HandleCreateNotifier godoc
//
// @Summary Create Notifier
// @Tags Notifiers
// @Produce json
// @Param payload body repo.NotifierCreate true "Notifier Data"
// @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers [POST]
// @Security Bearer
func (ctrl *V1Controller) HandleCreateNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, in repo.NotifierCreate) (repo.NotifierOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Notifiers.Create(auth, auth.GID, auth.UID, in)
}
return adapters.Action(fn, http.StatusCreated)
}
// HandleDeleteNotifier godocs
//
// @Summary Delete a Notifier
// @Tags Notifiers
// @Param id path string true "Notifier ID"
// @Success 204
// @Router /v1/notifiers/{id} [DELETE]
// @Security Bearer
func (ctrl *V1Controller) HandleDeleteNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
auth := services.NewContext(r.Context())
return nil, ctrl.repo.Notifiers.Delete(auth, auth.UID, ID)
}
return adapters.CommandID("id", fn, http.StatusNoContent)
}
// HandleUpdateNotifier godocs
//
// @Summary Update Notifier
// @Tags Notifiers
// @Param id path string true "Notifier ID"
// @Param payload body repo.NotifierUpdate true "Notifier Data"
// @Success 200 {object} repo.NotifierOut
// @Router /v1/notifiers/{id} [PUT]
// @Security Bearer
func (ctrl *V1Controller) HandleUpdateNotifier() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID, in repo.NotifierUpdate) (repo.NotifierOut, error) {
auth := services.NewContext(r.Context())
return ctrl.repo.Notifiers.Update(auth, auth.UID, ID, in)
}
return adapters.ActionID("id", fn, http.StatusOK)
}
// HandlerNotifierTest godoc
//
// @Summary Test Notifier
// @Tags Notifiers
// @Produce json
// @Param id path string true "Notifier ID"
// @Param url query string true "URL"
// @Success 204
// @Router /v1/notifiers/test [POST]
// @Security Bearer
func (ctrl *V1Controller) HandlerNotifierTest() errchain.HandlerFunc {
type body struct {
URL string `json:"url" validate:"required"`
}
fn := func(r *http.Request, q body) (any, error) {
err := shoutrrr.Send(q.URL, "Test message from Homebox")
return nil, err
}
return adapters.Action(fn, http.StatusOK)
}

View file

@ -6,8 +6,8 @@ import (
"io" "io"
"net/http" "net/http"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/yeqown/go-qrcode/v2" "github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard" "github.com/yeqown/go-qrcode/writer/standard"
@ -19,31 +19,32 @@ var qrcodeLogo []byte
// HandleGenerateQRCode godoc // HandleGenerateQRCode godoc
// //
// @Summary Create QR Code // @Summary Encode data into QRCode
// @Tags Items // @Tags Items
// @Produce json // @Produce json
// @Param data query string false "data to be encoded into qrcode" // @Param data query string false "data to be encoded into qrcode"
// @Success 200 {string} string "image/jpeg" // @Success 200 {string} string "image/jpeg"
// @Router /v1/qrcode [GET] // @Router /v1/qrcode [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGenerateQRCode() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGenerateQRCode() server.HandlerFunc {
type query struct { const MaxLength = 4_296 // assume alphanumeric characters only
// 4,296 characters is the maximum length of a QR code
Data string `schema:"data" validate:"required,max=4296"`
}
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
q, err := adapters.DecodeQuery[query](r) data := r.URL.Query().Get("data")
if err != nil {
return err
}
image, err := png.Decode(bytes.NewReader(qrcodeLogo)) image, err := png.Decode(bytes.NewReader(qrcodeLogo))
if err != nil { if err != nil {
panic(err) panic(err)
} }
qrc, err := qrcode.New(q.Data) if len(data) > MaxLength {
return validate.NewFieldErrors(validate.FieldError{
Field: "data",
Error: "max length is 4,296 characters exceeded",
})
}
qrc, err := qrcode.New(data)
if err != nil { if err != nil {
return err return err
} }

View file

@ -4,28 +4,28 @@ import (
"net/http" "net/http"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
) )
// HandleBillOfMaterialsExport godoc // HandleBillOfMaterialsExport godoc
// //
// @Summary Export Bill of Materials // @Summary Generates a Bill of Materials CSV
// @Tags Reporting // @Tags Reporting
// @Produce json // @Produce json
// @Success 200 {string} string "text/csv" // @Success 200 {string} string "text/csv"
// @Router /v1/reporting/bill-of-materials [GET] // @Router /v1/reporting/bill-of-materials [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleBillOfMaterialsExport() errchain.HandlerFunc { func (ctrl *V1Controller) HandleBillOfMaterialsExport() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
actor := services.UseUserCtx(r.Context()) actor := services.UseUserCtx(r.Context())
csv, err := ctrl.svc.Items.ExportBillOfMaterialsTSV(r.Context(), actor.GroupID) csv, err := ctrl.svc.Reporting.BillOfMaterialsTSV(r.Context(), actor.GroupID)
if err != nil { if err != nil {
return err return err
} }
w.Header().Set("Content-Type", "text/tsv") w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", "attachment; filename=bill-of-materials.tsv") w.Header().Set("Content-Disposition", "attachment; filename=bom.csv")
_, err = w.Write(csv) _, err = w.Write(csv)
return err return err
} }

View file

@ -5,67 +5,72 @@ import (
"time" "time"
"github.com/hay-kot/homebox/backend/internal/core/services" "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/internal/sys/validate"
"github.com/hay-kot/homebox/backend/internal/web/adapters" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/errchain"
"github.com/hay-kot/httpkit/server"
) )
// HandleGroupGet godoc // HandleGroupGet godoc
// // @Summary Get the current user's group statistics
// @Summary Get Location Statistics
// @Tags Statistics // @Tags Statistics
// @Produce json // @Produce json
// @Success 200 {object} []repo.TotalsByOrganizer // @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/locations [GET] // @Router /v1/groups/statistics/locations [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsLocations() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGroupStatisticsLocations() server.HandlerFunc {
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) ctx := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsLocationsByPurchasePrice(auth, auth.GID)
stats, err := ctrl.repo.Groups.StatsLocationsByPurchasePrice(ctx, ctx.GID)
if err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return adapters.Command(fn, http.StatusOK) return server.Respond(w, http.StatusOK, stats)
}
} }
// HandleGroupStatisticsLabels godoc // HandleGroupGet godoc
// // @Summary Get the current user's group statistics
// @Summary Get Label Statistics
// @Tags Statistics // @Tags Statistics
// @Produce json // @Produce json
// @Success 200 {object} []repo.TotalsByOrganizer // @Success 200 {object} []repo.TotalsByOrganizer
// @Router /v1/groups/statistics/labels [GET] // @Router /v1/groups/statistics/labels [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsLabels() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGroupStatisticsLabels() server.HandlerFunc {
fn := func(r *http.Request) ([]repo.TotalsByOrganizer, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) ctx := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsLabelsByPurchasePrice(auth, auth.GID)
stats, err := ctrl.repo.Groups.StatsLabelsByPurchasePrice(ctx, ctx.GID)
if err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return adapters.Command(fn, http.StatusOK) return server.Respond(w, http.StatusOK, stats)
}
} }
// HandleGroupStatistics godoc // HandleGroupGet godoc
// // @Summary Get the current user's group statistics
// @Summary Get Group Statistics
// @Tags Statistics // @Tags Statistics
// @Produce json // @Produce json
// @Success 200 {object} repo.GroupStatistics // @Success 200 {object} repo.GroupStatistics
// @Router /v1/groups/statistics [GET] // @Router /v1/groups/statistics [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGroupStatistics() server.HandlerFunc {
fn := func(r *http.Request) (repo.GroupStatistics, error) { return func(w http.ResponseWriter, r *http.Request) error {
auth := services.NewContext(r.Context()) ctx := services.NewContext(r.Context())
return ctrl.repo.Groups.StatsGroup(auth, auth.GID)
stats, err := ctrl.repo.Groups.StatsGroup(ctx, ctx.GID)
if err != nil {
return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return adapters.Command(fn, http.StatusOK) return server.Respond(w, http.StatusOK, stats)
}
} }
// HandleGroupStatisticsPriceOverTime godoc // HandleGroupGet godoc
// // @Summary Queries the changes overtime of the purchase price over time
// @Summary Get Purchase Price Statistics
// @Tags Statistics // @Tags Statistics
// @Produce json // @Produce json
// @Success 200 {object} repo.ValueOverTime // @Success 200 {object} repo.ValueOverTime
@ -73,7 +78,7 @@ func (ctrl *V1Controller) HandleGroupStatistics() errchain.HandlerFunc {
// @Param end query string false "end date" // @Param end query string false "end date"
// @Router /v1/groups/statistics/purchase-price [GET] // @Router /v1/groups/statistics/purchase-price [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() errchain.HandlerFunc { func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() server.HandlerFunc {
parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) { parseDate := func(datestr string, defaultDate time.Time) (time.Time, error) {
if datestr == "" { if datestr == "" {
return defaultDate, nil return defaultDate, nil
@ -99,6 +104,6 @@ func (ctrl *V1Controller) HandleGroupStatisticsPriceOverTime() errchain.HandlerF
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusOK, stats) return server.Respond(w, http.StatusOK, stats)
} }
} }

View file

@ -8,20 +8,18 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services" "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/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// HandleUserRegistration godoc // HandleUserSelf godoc
// // @Summary Get the current user
// @Summary Register New User
// @Tags User // @Tags User
// @Produce json // @Produce json
// @Param payload body services.UserRegistration true "User Data" // @Param payload body services.UserRegistration true "User Data"
// @Success 204 // @Success 204
// @Router /v1/users/register [Post] // @Router /v1/users/register [Post]
func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc { func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
regData := services.UserRegistration{} regData := services.UserRegistration{}
@ -40,19 +38,18 @@ func (ctrl *V1Controller) HandleUserRegistration() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusNoContent, nil) return server.Respond(w, http.StatusNoContent, nil)
} }
} }
// HandleUserSelf godoc // HandleUserSelf godoc
// // @Summary Get the current user
// @Summary Get User Self
// @Tags User // @Tags User
// @Produce json // @Produce json
// @Success 200 {object} Wrapped{item=repo.UserOut} // @Success 200 {object} server.Result{item=repo.UserOut}
// @Router /v1/users/self [GET] // @Router /v1/users/self [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc { func (ctrl *V1Controller) HandleUserSelf() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
token := services.UseTokenCtx(r.Context()) token := services.UseTokenCtx(r.Context())
usr, err := ctrl.svc.User.GetSelf(r.Context(), token) usr, err := ctrl.svc.User.GetSelf(r.Context(), token)
@ -61,20 +58,19 @@ func (ctrl *V1Controller) HandleUserSelf() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusOK, Wrap(usr)) return server.Respond(w, http.StatusOK, server.Wrap(usr))
} }
} }
// HandleUserSelfUpdate godoc // HandleUserSelfUpdate godoc
// // @Summary Update the current user
// @Summary Update Account
// @Tags User // @Tags User
// @Produce json // @Produce json
// @Param payload body repo.UserUpdate true "User Data" // @Param payload body repo.UserUpdate true "User Data"
// @Success 200 {object} Wrapped{item=repo.UserUpdate} // @Success 200 {object} server.Result{item=repo.UserUpdate}
// @Router /v1/users/self [PUT] // @Router /v1/users/self [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc { func (ctrl *V1Controller) HandleUserSelfUpdate() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
updateData := repo.UserUpdate{} updateData := repo.UserUpdate{}
if err := server.Decode(r, &updateData); err != nil { if err := server.Decode(r, &updateData); err != nil {
@ -88,19 +84,18 @@ func (ctrl *V1Controller) HandleUserSelfUpdate() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusOK, Wrap(newData)) return server.Respond(w, http.StatusOK, server.Wrap(newData))
} }
} }
// HandleUserSelfDelete godoc // HandleUserSelfDelete godoc
// // @Summary Deletes the user account
// @Summary Delete Account
// @Tags User // @Tags User
// @Produce json // @Produce json
// @Success 204 // @Success 204
// @Router /v1/users/self [DELETE] // @Router /v1/users/self [DELETE]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc { func (ctrl *V1Controller) HandleUserSelfDelete() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
if ctrl.isDemo { if ctrl.isDemo {
return validate.NewRequestError(nil, http.StatusForbidden) return validate.NewRequestError(nil, http.StatusForbidden)
@ -111,7 +106,7 @@ func (ctrl *V1Controller) HandleUserSelfDelete() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusNoContent, nil) return server.Respond(w, http.StatusNoContent, nil)
} }
} }
@ -123,14 +118,13 @@ type (
) )
// HandleUserSelfChangePassword godoc // HandleUserSelfChangePassword godoc
// // @Summary Updates the users password
// @Summary Change Password
// @Tags User // @Tags User
// @Success 204 // @Success 204
// @Param payload body ChangePassword true "Password Payload" // @Param payload body ChangePassword true "Password Payload"
// @Router /v1/users/change-password [PUT] // @Router /v1/users/change-password [PUT]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc { func (ctrl *V1Controller) HandleUserSelfChangePassword() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
if ctrl.isDemo { if ctrl.isDemo {
return validate.NewRequestError(nil, http.StatusForbidden) return validate.NewRequestError(nil, http.StatusForbidden)
@ -149,6 +143,6 @@ func (ctrl *V1Controller) HandleUserSelfChangePassword() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)
} }
return server.JSON(w, http.StatusNoContent, nil) return server.Respond(w, http.StatusNoContent, nil)
} }
} }

View file

@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -10,23 +9,16 @@ import (
atlas "ariga.io/atlas/sql/migrate" atlas "ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect/sql/schema" "entgo.io/ent/dialect/sql/schema"
"github.com/go-chi/chi/v5" "github.com/hay-kot/homebox/backend/app/api/static/docs"
"github.com/go-chi/chi/v5/middleware"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/migrations" "github.com/hay-kot/homebox/backend/internal/data/migrations"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/sys/config" "github.com/hay-kot/homebox/backend/internal/sys/config"
"github.com/hay-kot/homebox/backend/internal/web/mid" "github.com/hay-kot/homebox/backend/internal/web/mid"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
"github.com/hay-kot/httpkit/server" _ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"
_ "github.com/hay-kot/homebox/backend/pkgs/cgofreesqlite"
) )
var ( var (
@ -35,23 +27,24 @@ var (
buildTime = "now" buildTime = "now"
) )
// @title Homebox API // @title Go API Templates
// @version 1.0 // @version 1.0
// @description Track, Manage, and Organize your Things. // @description This is a simple Rest API Server Template that implements some basic User and Authentication patterns to help you get started and bootstrap your next project!.
// @contact.name Don't // @contact.name Don't
// @license.name MIT
// @BasePath /api // @BasePath /api
// @securityDefinitions.apikey Bearer // @securityDefinitions.apikey Bearer
// @in header // @in header
// @name Authorization // @name Authorization
// @description "Type 'Bearer TOKEN' to correctly set the API Key" // @description "Type 'Bearer TOKEN' to correctly set the API Key"
func main() { func main() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
cfg, err := config.New() cfg, err := config.New()
if err != nil { if err != nil {
panic(err) panic(err)
} }
docs.SwaggerInfo.Host = cfg.Swagger.Host
if err := run(cfg); err != nil { if err := run(cfg); err != nil {
panic(err) panic(err)
} }
@ -117,43 +110,39 @@ func run(cfg *config.Config) error {
return err return err
} }
app.bus = eventbus.New()
app.db = c app.db = c
app.repos = repo.New(c, app.bus, cfg.Storage.Data) app.repos = repo.New(c, cfg.Storage.Data)
app.services = services.New( app.services = services.New(
app.repos, app.repos,
services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID), services.WithAutoIncrementAssetID(cfg.Options.AutoIncrementAssetID),
) )
// ========================================================================= // =========================================================================
// Start Server // Start Server\
logger := log.With().Caller().Logger() logger := log.With().Caller().Logger()
router := chi.NewMux() mwLogger := mid.Logger(logger)
router.Use( if app.conf.Mode == config.ModeDevelopment {
middleware.RequestID, mwLogger = mid.SugarLogger(logger)
middleware.RealIP, }
mid.Logger(logger),
middleware.Recoverer,
middleware.StripSlashes,
)
chain := errchain.New(mid.Errors(app.server, logger))
app.mountRoutes(router, chain, app.repos)
app.server = server.NewServer( app.server = server.NewServer(
server.WithHost(app.conf.Web.Host), server.WithHost(app.conf.Web.Host),
server.WithPort(app.conf.Web.Port), server.WithPort(app.conf.Web.Port),
server.WithMiddleware(
mwLogger,
mid.Errors(logger),
mid.Panic(app.conf.Mode == config.ModeDevelopment),
),
) )
app.mountRoutes(app.repos)
log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port) log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port)
// ========================================================================= // =========================================================================
// Start Reoccurring Tasks // Start Reoccurring Tasks
go app.bus.Run()
go app.startBgTask(time.Duration(24)*time.Hour, func() { go app.startBgTask(time.Duration(24)*time.Hour, func() {
_, err := app.repos.AuthTokens.PurgeExpiredTokens(context.Background()) _, err := app.repos.AuthTokens.PurgeExpiredTokens(context.Background())
if err != nil { if err != nil {
@ -170,19 +159,6 @@ func run(cfg *config.Config) error {
Msg("failed to purge expired invitations") Msg("failed to purge expired invitations")
} }
}) })
go app.startBgTask(time.Duration(1)*time.Hour, func() {
now := time.Now()
if now.Hour() == 8 {
fmt.Println("run notifiers")
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
if err != nil {
log.Error().
Err(err).
Msg("failed to send notifiers")
}
}
})
// TODO: Remove through external API that does setup // TODO: Remove through external API that does setup
if cfg.Demo { if cfg.Demo {
@ -199,5 +175,5 @@ func run(cfg *config.Config) error {
}() }()
} }
return app.server.Start(router) return app.server.Start()
} }

View file

@ -7,10 +7,9 @@ import (
"net/url" "net/url"
"strings" "strings"
v1 "github.com/hay-kot/homebox/backend/app/api/handlers/v1"
"github.com/hay-kot/homebox/backend/internal/core/services" "github.com/hay-kot/homebox/backend/internal/core/services"
"github.com/hay-kot/homebox/backend/internal/sys/validate" "github.com/hay-kot/homebox/backend/internal/sys/validate"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
) )
type tokenHasKey struct { type tokenHasKey struct {
@ -31,9 +30,9 @@ const (
// the required roles, a 403 Forbidden will be returned. // the required roles, a 403 Forbidden will be returned.
// //
// WARNING: This middleware _MUST_ be called after mwAuthToken or else it will panic // WARNING: This middleware _MUST_ be called after mwAuthToken or else it will panic
func (a *app) mwRoles(rm RoleMode, required ...string) errchain.Middleware { func (a *app) mwRoles(rm RoleMode, required ...string) server.Middleware {
return func(next errchain.Handler) errchain.Handler { return func(next server.Handler) server.Handler {
return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context() ctx := r.Context()
maybeToken := ctx.Value(hashedToken) maybeToken := ctx.Value(hashedToken)
@ -95,6 +94,20 @@ func getQuery(r *http.Request) (string, error) {
return token, nil return token, nil
} }
func getCookie(r *http.Request) (string, error) {
cookie, err := r.Cookie("hb.auth.token")
if err != nil {
return "", errors.New("access_token cookie is required")
}
token, err := url.QueryUnescape(cookie.Value)
if err != nil {
return "", errors.New("access_token cookie is required")
}
return token, nil
}
// mwAuthToken is a middleware that will check the database for a stateful token // mwAuthToken is a middleware that will check the database for a stateful token
// and attach it's user to the request context, or return an appropriate error. // and attach it's user to the request context, or return an appropriate error.
// Authorization support is by token via Headers or Query Parameter // Authorization support is by token via Headers or Query Parameter
@ -102,24 +115,16 @@ func getQuery(r *http.Request) (string, error) {
// Example: // Example:
// - header = "Bearer 1234567890" // - header = "Bearer 1234567890"
// - query = "?access_token=1234567890" // - query = "?access_token=1234567890"
func (a *app) mwAuthToken(next errchain.Handler) errchain.Handler { // - cookie = hb.auth.token = 1234567890
return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { func (a *app) mwAuthToken(next server.Handler) server.Handler {
var requestToken string return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
// We ignore the error to allow the next strategy to be attempted
{
cookies, _ := v1.GetCookies(r)
if cookies != nil {
requestToken = cookies.Token
}
}
if requestToken == "" {
keyFuncs := [...]KeyFunc{ keyFuncs := [...]KeyFunc{
getBearer, getBearer,
getCookie,
getQuery, getQuery,
} }
var requestToken string
for _, keyFunc := range keyFuncs { for _, keyFunc := range keyFuncs {
token, err := keyFunc(r) token, err := keyFunc(r)
if err == nil { if err == nil {
@ -127,7 +132,6 @@ func (a *app) mwAuthToken(next errchain.Handler) errchain.Handler {
break break
} }
} }
}
if requestToken == "" { if requestToken == "" {
return validate.NewRequestError(errors.New("Authorization header or query is required"), http.StatusUnauthorized) return validate.NewRequestError(errors.New("Authorization header or query is required"), http.StatusUnauthorized)

View file

@ -3,19 +3,19 @@ package main
import ( import (
"embed" "embed"
"errors" "errors"
"fmt"
"io" "io"
"mime" "mime"
"net/http" "net/http"
"path" "path"
"path/filepath" "path/filepath"
"github.com/go-chi/chi/v5"
"github.com/hay-kot/homebox/backend/app/api/handlers/debughandlers" "github.com/hay-kot/homebox/backend/app/api/handlers/debughandlers"
v1 "github.com/hay-kot/homebox/backend/app/api/handlers/v1" v1 "github.com/hay-kot/homebox/backend/app/api/handlers/v1"
_ "github.com/hay-kot/homebox/backend/app/api/static/docs" _ "github.com/hay-kot/homebox/backend/app/api/static/docs"
"github.com/hay-kot/homebox/backend/internal/data/ent/authroles" "github.com/hay-kot/homebox/backend/internal/data/ent/authroles"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/httpkit/errchain" "github.com/hay-kot/homebox/backend/pkgs/server"
httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware
) )
@ -36,12 +36,12 @@ func (a *app) debugRouter() *http.ServeMux {
} }
// registerRoutes registers all the routes for the API // registerRoutes registers all the routes for the API
func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllRepos) { func (a *app) mountRoutes(repos *repo.AllRepos) {
registerMimes() registerMimes()
r.Get("/swagger/*", httpSwagger.Handler( a.server.Get("/swagger/*", server.ToHandler(httpSwagger.Handler(
httpSwagger.URL("/swagger/doc.json"), httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)),
)) )))
// ========================================================================= // =========================================================================
// API Version 1 // API Version 1
@ -51,111 +51,95 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
v1Ctrl := v1.NewControllerV1( v1Ctrl := v1.NewControllerV1(
a.services, a.services,
a.repos, a.repos,
a.bus,
v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize), v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
v1.WithRegistration(a.conf.Options.AllowRegistration), v1.WithRegistration(a.conf.Options.AllowRegistration),
v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
) )
r.Get(v1Base("/status"), chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{ a.server.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
Version: version, Version: version,
Commit: commit, Commit: commit,
BuildTime: buildTime, BuildTime: buildTime,
}))) }))
r.Post(v1Base("/users/register"), chain.ToHandlerFunc(v1Ctrl.HandleUserRegistration())) a.server.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
r.Post(v1Base("/users/login"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogin())) a.server.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
userMW := []errchain.Middleware{ userMW := []server.Middleware{
a.mwAuthToken, a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String()), a.mwRoles(RoleModeOr, authroles.RoleUser.String()),
} }
r.Get(v1Base("/ws/events"), chain.ToHandlerFunc(v1Ctrl.HandleCacheWS(), userMW...)) a.server.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf(), userMW...)
r.Get(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelf(), userMW...)) a.server.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate(), userMW...)
r.Put(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfUpdate(), userMW...)) a.server.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete(), userMW...)
r.Delete(v1Base("/users/self"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfDelete(), userMW...)) a.server.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout(), userMW...)
r.Post(v1Base("/users/logout"), chain.ToHandlerFunc(v1Ctrl.HandleAuthLogout(), userMW...)) a.server.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh(), userMW...)
r.Get(v1Base("/users/refresh"), chain.ToHandlerFunc(v1Ctrl.HandleAuthRefresh(), userMW...)) a.server.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword(), userMW...)
r.Put(v1Base("/users/self/change-password"), chain.ToHandlerFunc(v1Ctrl.HandleUserSelfChangePassword(), userMW...))
r.Post(v1Base("/groups/invitations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupInvitationsCreate(), userMW...)) a.server.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate(), userMW...)
r.Get(v1Base("/groups/statistics"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatistics(), userMW...)) a.server.Get(v1Base("/groups/statistics"), v1Ctrl.HandleGroupStatistics(), userMW...)
r.Get(v1Base("/groups/statistics/purchase-price"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...)) a.server.Get(v1Base("/groups/statistics/purchase-price"), v1Ctrl.HandleGroupStatisticsPriceOverTime(), userMW...)
r.Get(v1Base("/groups/statistics/locations"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLocations(), userMW...)) a.server.Get(v1Base("/groups/statistics/locations"), v1Ctrl.HandleGroupStatisticsLocations(), userMW...)
r.Get(v1Base("/groups/statistics/labels"), chain.ToHandlerFunc(v1Ctrl.HandleGroupStatisticsLabels(), userMW...)) a.server.Get(v1Base("/groups/statistics/labels"), v1Ctrl.HandleGroupStatisticsLabels(), userMW...)
// TODO: I don't like /groups being the URL for users // TODO: I don't like /groups being the URL for users
r.Get(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupGet(), userMW...)) a.server.Get(v1Base("/groups"), v1Ctrl.HandleGroupGet(), userMW...)
r.Put(v1Base("/groups"), chain.ToHandlerFunc(v1Ctrl.HandleGroupUpdate(), userMW...)) a.server.Put(v1Base("/groups"), v1Ctrl.HandleGroupUpdate(), userMW...)
r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...)) a.server.Post(v1Base("/actions/ensure-asset-ids"), v1Ctrl.HandleEnsureAssetID(), userMW...)
r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...)) a.server.Post(v1Base("/actions/zero-item-time-fields"), v1Ctrl.HandleItemDateZeroOut(), userMW...)
r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
r.Post(v1Base("/actions/set-primary-photos"), chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...)) a.server.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll(), userMW...)
r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...)) a.server.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate(), userMW...)
r.Get(v1Base("/locations/tree"), chain.ToHandlerFunc(v1Ctrl.HandleLocationTreeQuery(), userMW...)) a.server.Get(v1Base("/locations/tree"), v1Ctrl.HandleLocationTreeQuery(), userMW...)
r.Get(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGet(), userMW...)) a.server.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet(), userMW...)
r.Put(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationUpdate(), userMW...)) a.server.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate(), userMW...)
r.Delete(v1Base("/locations/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLocationDelete(), userMW...)) a.server.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete(), userMW...)
r.Get(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsGetAll(), userMW...)) a.server.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll(), userMW...)
r.Post(v1Base("/labels"), chain.ToHandlerFunc(v1Ctrl.HandleLabelsCreate(), userMW...)) a.server.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate(), userMW...)
r.Get(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelGet(), userMW...)) a.server.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet(), userMW...)
r.Put(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelUpdate(), userMW...)) a.server.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate(), userMW...)
r.Delete(v1Base("/labels/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleLabelDelete(), userMW...)) a.server.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete(), userMW...)
r.Get(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsGetAll(), userMW...)) a.server.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll(), userMW...)
r.Post(v1Base("/items"), chain.ToHandlerFunc(v1Ctrl.HandleItemsCreate(), userMW...)) a.server.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate(), userMW...)
r.Post(v1Base("/items/import"), chain.ToHandlerFunc(v1Ctrl.HandleItemsImport(), userMW...)) a.server.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport(), userMW...)
r.Get(v1Base("/items/export"), chain.ToHandlerFunc(v1Ctrl.HandleItemsExport(), userMW...)) a.server.Get(v1Base("/items/fields"), v1Ctrl.HandleGetAllCustomFieldNames(), userMW...)
r.Get(v1Base("/items/fields"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldNames(), userMW...)) a.server.Get(v1Base("/items/fields/values"), v1Ctrl.HandleGetAllCustomFieldValues(), userMW...)
r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...)) a.server.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet(), userMW...)
r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...)) a.server.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate(), userMW...)
r.Patch(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...)) a.server.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete(), userMW...)
r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))
r.Post(v1Base("/items/{id}/attachments"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...)) a.server.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate(), userMW...)
r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...)) a.server.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate(), userMW...)
r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentDelete(), userMW...)) a.server.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete(), userMW...)
r.Get(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceLogGet(), userMW...)) a.server.Get(v1Base("/items/{id}/maintenance"), v1Ctrl.HandleMaintenanceEntryCreate(), userMW...)
r.Post(v1Base("/items/{id}/maintenance"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryCreate(), userMW...)) a.server.Post(v1Base("/items/{id}/maintenance"), v1Ctrl.HandleMaintenanceEntryCreate(), userMW...)
r.Put(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...)) a.server.Put(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...)
r.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryDelete(), userMW...)) a.server.Delete(v1Base("/items/{id}/maintenance/{entry_id}"), v1Ctrl.HandleMaintenanceEntryDelete(), userMW...)
r.Get(v1Base("/assets/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...)) a.server.Get(v1Base("/asset/{id}"), v1Ctrl.HandleAssetGet(), userMW...)
// Notifiers
r.Get(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleGetUserNotifiers(), userMW...))
r.Post(v1Base("/notifiers"), chain.ToHandlerFunc(v1Ctrl.HandleCreateNotifier(), userMW...))
r.Put(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleUpdateNotifier(), userMW...))
r.Delete(v1Base("/notifiers/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleDeleteNotifier(), userMW...))
r.Post(v1Base("/notifiers/test"), chain.ToHandlerFunc(v1Ctrl.HandlerNotifierTest(), userMW...))
// Asset-Like endpoints // Asset-Like endpoints
assetMW := []errchain.Middleware{ a.server.Get(
a.mwAuthToken,
a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
}
r.Get(
v1Base("/qrcode"), v1Base("/qrcode"),
chain.ToHandlerFunc(v1Ctrl.HandleGenerateQRCode(), assetMW...), v1Ctrl.HandleGenerateQRCode(),
a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
) )
r.Get( a.server.Get(
v1Base("/items/{id}/attachments/{attachment_id}"), v1Base("/items/{id}/attachments/{attachment_id}"),
chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentGet(), assetMW...), v1Ctrl.HandleItemAttachmentGet(),
a.mwAuthToken, a.mwRoles(RoleModeOr, authroles.RoleUser.String(), authroles.RoleAttachments.String()),
) )
// Reporting Services // Reporting Services
r.Get(v1Base("/reporting/bill-of-materials"), chain.ToHandlerFunc(v1Ctrl.HandleBillOfMaterialsExport(), userMW...)) a.server.Get(v1Base("/reporting/bill-of-materials"), v1Ctrl.HandleBillOfMaterialsExport(), userMW...)
r.NotFound(chain.ToHandlerFunc(notFoundHandler())) a.server.NotFound(notFoundHandler())
} }
func registerMimes() { func registerMimes() {
@ -172,7 +156,7 @@ func registerMimes() {
// notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that // notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that
// the client side routing is handled correctly. // the client side routing is handled correctly.
func notFoundHandler() errchain.HandlerFunc { func notFoundHandler() server.HandlerFunc {
tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error { tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error {
f, err := fs.Open(path.Join(prefix, requestedPath)) f, err := fs.Open(path.Join(prefix, requestedPath))
if err != nil { if err != nil {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -52,8 +52,6 @@ definitions:
$ref: '#/definitions/repo.DocumentOut' $ref: '#/definitions/repo.DocumentOut'
id: id:
type: string type: string
primary:
type: boolean
type: type:
type: string type: string
updatedAt: updatedAt:
@ -61,8 +59,6 @@ definitions:
type: object type: object
repo.ItemAttachmentUpdate: repo.ItemAttachmentUpdate:
properties: properties:
primary:
type: boolean
title: title:
type: string type: string
type: type:
@ -71,7 +67,6 @@ definitions:
repo.ItemCreate: repo.ItemCreate:
properties: properties:
description: description:
maxLength: 1000
type: string type: string
labelIds: labelIds:
items: items:
@ -81,14 +76,10 @@ definitions:
description: Edges description: Edges
type: string type: string
name: name:
maxLength: 255
minLength: 1
type: string type: string
parentId: parentId:
type: string type: string
x-nullable: true x-nullable: true
required:
- name
type: object type: object
repo.ItemField: repo.ItemField:
properties: properties:
@ -130,8 +121,6 @@ definitions:
type: array type: array
id: id:
type: string type: string
imageId:
type: string
insured: insured:
type: boolean type: boolean
labels: labels:
@ -188,15 +177,6 @@ definitions:
warrantyExpires: warrantyExpires:
type: string type: string
type: object type: object
repo.ItemPatch:
properties:
id:
type: string
quantity:
type: integer
x-nullable: true
x-omitempty: true
type: object
repo.ItemSummary: repo.ItemSummary:
properties: properties:
archived: archived:
@ -207,8 +187,6 @@ definitions:
type: string type: string
id: id:
type: string type: string
imageId:
type: string
insured: insured:
type: boolean type: boolean
labels: labels:
@ -303,14 +281,9 @@ definitions:
color: color:
type: string type: string
description: description:
maxLength: 255
type: string type: string
name: name:
maxLength: 255
minLength: 1
type: string type: string
required:
- name
type: object type: object
repo.LabelOut: repo.LabelOut:
properties: properties:
@ -320,6 +293,10 @@ definitions:
type: string type: string
id: id:
type: string type: string
items:
items:
$ref: '#/definitions/repo.ItemSummary'
type: array
name: name:
type: string type: string
updatedAt: updatedAt:
@ -360,6 +337,10 @@ definitions:
type: string type: string
id: id:
type: string type: string
items:
items:
$ref: '#/definitions/repo.ItemSummary'
type: array
name: name:
type: string type: string
parent: parent:
@ -409,55 +390,41 @@ definitions:
type: object type: object
repo.MaintenanceEntry: repo.MaintenanceEntry:
properties: properties:
completedDate:
description: Sold
type: string
cost: cost:
example: "0" example: "0"
type: string type: string
date:
type: string
description: description:
type: string type: string
id: id:
type: string type: string
name: name:
type: string type: string
scheduledDate:
description: Sold
type: string
type: object type: object
repo.MaintenanceEntryCreate: repo.MaintenanceEntryCreate:
properties: properties:
completedDate:
description: Sold
type: string
cost: cost:
example: "0" example: "0"
type: string type: string
date:
type: string
description: description:
type: string type: string
name: name:
type: string type: string
scheduledDate:
description: Sold
type: string
required:
- name
type: object type: object
repo.MaintenanceEntryUpdate: repo.MaintenanceEntryUpdate:
properties: properties:
completedDate:
description: Sold
type: string
cost: cost:
example: "0" example: "0"
type: string type: string
date:
type: string
description: description:
type: string type: string
name: name:
type: string type: string
scheduledDate:
description: Sold
type: string
type: object type: object
repo.MaintenanceLog: repo.MaintenanceLog:
properties: properties:
@ -472,51 +439,6 @@ definitions:
itemId: itemId:
type: string type: string
type: object type: object
repo.NotifierCreate:
properties:
isActive:
type: boolean
name:
maxLength: 255
minLength: 1
type: string
url:
type: string
required:
- name
- url
type: object
repo.NotifierOut:
properties:
createdAt:
type: string
groupId:
type: string
id:
type: string
isActive:
type: boolean
name:
type: string
updatedAt:
type: string
userId:
type: string
type: object
repo.NotifierUpdate:
properties:
isActive:
type: boolean
name:
maxLength: 255
minLength: 1
type: string
url:
type: string
x-nullable: true
required:
- name
type: object
repo.PaginationResult-repo_ItemSummary: repo.PaginationResult-repo_ItemSummary:
properties: properties:
items: items:
@ -600,6 +522,28 @@ definitions:
value: value:
type: number type: number
type: object type: object
server.ErrorResponse:
properties:
error:
type: string
fields:
additionalProperties:
type: string
type: object
type: object
server.Result:
properties:
details: {}
error:
type: boolean
item: {}
message:
type: string
type: object
server.Results:
properties:
items: {}
type: object
services.UserRegistration: services.UserRegistration:
properties: properties:
email: email:
@ -665,26 +609,13 @@ definitions:
expiresAt: expiresAt:
type: string type: string
uses: uses:
maximum: 100
minimum: 1
type: integer type: integer
required:
- uses
type: object type: object
v1.ItemAttachmentToken: v1.ItemAttachmentToken:
properties: properties:
token: token:
type: string type: string
type: object type: object
v1.LoginForm:
properties:
password:
type: string
stayLoggedIn:
type: boolean
username:
type: string
type: object
v1.TokenResponse: v1.TokenResponse:
properties: properties:
attachmentToken: attachmentToken:
@ -694,27 +625,19 @@ definitions:
token: token:
type: string type: string
type: object type: object
v1.Wrapped:
properties:
item: {}
type: object
validate.ErrorResponse:
properties:
error:
type: string
fields:
type: string
type: object
info: info:
contact: contact:
name: Don't name: Don't
description: Track, Manage, and Organize your Things. description: This is a simple Rest API Server Template that implements some basic
title: Homebox API User and Authentication patterns to help you get started and bootstrap your next
project!.
license:
name: MIT
title: Go API Templates
version: "1.0" version: "1.0"
paths: paths:
/v1/actions/ensure-asset-ids: /v1/actions/ensure-asset-ids:
post: post:
description: Ensures all items in the database have an asset ID
produces: produces:
- application/json - application/json
responses: responses:
@ -724,42 +647,11 @@ paths:
$ref: '#/definitions/v1.ActionAmountResult' $ref: '#/definitions/v1.ActionAmountResult'
security: security:
- Bearer: [] - Bearer: []
summary: Ensure Asset IDs summary: Ensures all items in the database have an asset id
tags: tags:
- Actions - Group
/v1/actions/ensure-import-refs:
post:
description: Ensures all items in the database have an import ref
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/v1.ActionAmountResult'
security:
- Bearer: []
summary: Ensures Import Refs
tags:
- Actions
/v1/actions/set-primary-photos:
post:
description: Sets the first photo of each item as the primary photo
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/v1.ActionAmountResult'
security:
- Bearer: []
summary: Set Primary Photos
tags:
- Actions
/v1/actions/zero-item-time-fields: /v1/actions/zero-item-time-fields:
post: post:
description: Resets all item date fields to the beginning of the day
produces: produces:
- application/json - application/json
responses: responses:
@ -769,9 +661,9 @@ paths:
$ref: '#/definitions/v1.ActionAmountResult' $ref: '#/definitions/v1.ActionAmountResult'
security: security:
- Bearer: [] - Bearer: []
summary: Zero Out Time Fields summary: Resets all item date fields to the beginning of the day
tags: tags:
- Actions - Group
/v1/assets/{id}: /v1/assets/{id}:
get: get:
parameters: parameters:
@ -789,9 +681,9 @@ paths:
$ref: '#/definitions/repo.PaginationResult-repo_ItemSummary' $ref: '#/definitions/repo.PaginationResult-repo_ItemSummary'
security: security:
- Bearer: [] - Bearer: []
summary: Get Item by Asset ID summary: Gets an item by Asset ID
tags: tags:
- Items - Assets
/v1/groups: /v1/groups:
get: get:
produces: produces:
@ -803,7 +695,7 @@ paths:
$ref: '#/definitions/repo.Group' $ref: '#/definitions/repo.Group'
security: security:
- Bearer: [] - Bearer: []
summary: Get Group summary: Get the current user's group
tags: tags:
- Group - Group
put: put:
@ -823,7 +715,7 @@ paths:
$ref: '#/definitions/repo.Group' $ref: '#/definitions/repo.Group'
security: security:
- Bearer: [] - Bearer: []
summary: Update Group summary: Updates some fields of the current users group
tags: tags:
- Group - Group
/v1/groups/invitations: /v1/groups/invitations:
@ -844,7 +736,7 @@ paths:
$ref: '#/definitions/v1.GroupInvitation' $ref: '#/definitions/v1.GroupInvitation'
security: security:
- Bearer: [] - Bearer: []
summary: Create Group Invitation summary: Get the current user
tags: tags:
- Group - Group
/v1/groups/statistics: /v1/groups/statistics:
@ -858,7 +750,7 @@ paths:
$ref: '#/definitions/repo.GroupStatistics' $ref: '#/definitions/repo.GroupStatistics'
security: security:
- Bearer: [] - Bearer: []
summary: Get Group Statistics summary: Get the current user's group statistics
tags: tags:
- Statistics - Statistics
/v1/groups/statistics/labels: /v1/groups/statistics/labels:
@ -874,7 +766,7 @@ paths:
type: array type: array
security: security:
- Bearer: [] - Bearer: []
summary: Get Label Statistics summary: Get the current user's group statistics
tags: tags:
- Statistics - Statistics
/v1/groups/statistics/locations: /v1/groups/statistics/locations:
@ -890,7 +782,7 @@ paths:
type: array type: array
security: security:
- Bearer: [] - Bearer: []
summary: Get Location Statistics summary: Get the current user's group statistics
tags: tags:
- Statistics - Statistics
/v1/groups/statistics/purchase-price: /v1/groups/statistics/purchase-price:
@ -913,7 +805,7 @@ paths:
$ref: '#/definitions/repo.ValueOverTime' $ref: '#/definitions/repo.ValueOverTime'
security: security:
- Bearer: [] - Bearer: []
summary: Get Purchase Price Statistics summary: Queries the changes overtime of the purchase price over time
tags: tags:
- Statistics - Statistics
/v1/items: /v1/items:
@ -954,7 +846,7 @@ paths:
$ref: '#/definitions/repo.PaginationResult-repo_ItemSummary' $ref: '#/definitions/repo.PaginationResult-repo_ItemSummary'
security: security:
- Bearer: [] - Bearer: []
summary: Query All Items summary: Get All Items
tags: tags:
- Items - Items
post: post:
@ -968,13 +860,13 @@ paths:
produces: produces:
- application/json - application/json
responses: responses:
"201": "200":
description: Created description: OK
schema: schema:
$ref: '#/definitions/repo.ItemSummary' $ref: '#/definitions/repo.ItemSummary'
security: security:
- Bearer: [] - Bearer: []
summary: Create Item summary: Create a new item
tags: tags:
- Items - Items
/v1/items/{id}: /v1/items/{id}:
@ -992,7 +884,7 @@ paths:
description: No Content description: No Content
security: security:
- Bearer: [] - Bearer: []
summary: Delete Item summary: deletes a item
tags: tags:
- Items - Items
get: get:
@ -1011,32 +903,7 @@ paths:
$ref: '#/definitions/repo.ItemOut' $ref: '#/definitions/repo.ItemOut'
security: security:
- Bearer: [] - Bearer: []
summary: Get Item summary: Gets a item and fields
tags:
- Items
patch:
parameters:
- description: Item ID
in: path
name: id
required: true
type: string
- description: Item Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.ItemPatch'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/repo.ItemOut'
security:
- Bearer: []
summary: Update Item
tags: tags:
- Items - Items
put: put:
@ -1061,7 +928,7 @@ paths:
$ref: '#/definitions/repo.ItemOut' $ref: '#/definitions/repo.ItemOut'
security: security:
- Bearer: [] - Bearer: []
summary: Update Item summary: updates a item
tags: tags:
- Items - Items
/v1/items/{id}/attachments: /v1/items/{id}/attachments:
@ -1097,10 +964,10 @@ paths:
"422": "422":
description: Unprocessable Entity description: Unprocessable Entity
schema: schema:
$ref: '#/definitions/validate.ErrorResponse' $ref: '#/definitions/server.ErrorResponse'
security: security:
- Bearer: [] - Bearer: []
summary: Create Item Attachment summary: imports items into the database
tags: tags:
- Items Attachments - Items Attachments
/v1/items/{id}/attachments/{attachment_id}: /v1/items/{id}/attachments/{attachment_id}:
@ -1121,7 +988,7 @@ paths:
description: No Content description: No Content
security: security:
- Bearer: [] - Bearer: []
summary: Delete Item Attachment summary: retrieves an attachment for an item
tags: tags:
- Items Attachments - Items Attachments
get: get:
@ -1145,7 +1012,7 @@ paths:
$ref: '#/definitions/v1.ItemAttachmentToken' $ref: '#/definitions/v1.ItemAttachmentToken'
security: security:
- Bearer: [] - Bearer: []
summary: Get Item Attachment summary: retrieves an attachment for an item
tags: tags:
- Items Attachments - Items Attachments
put: put:
@ -1173,7 +1040,7 @@ paths:
$ref: '#/definitions/repo.ItemOut' $ref: '#/definitions/repo.ItemOut'
security: security:
- Bearer: [] - Bearer: []
summary: Update Item Attachment summary: retrieves an attachment for an item
tags: tags:
- Items Attachments - Items Attachments
/v1/items/{id}/maintenance: /v1/items/{id}/maintenance:
@ -1201,8 +1068,8 @@ paths:
produces: produces:
- application/json - application/json
responses: responses:
"201": "200":
description: Created description: OK
schema: schema:
$ref: '#/definitions/repo.MaintenanceEntry' $ref: '#/definitions/repo.MaintenanceEntry'
security: security:
@ -1242,18 +1109,6 @@ paths:
summary: Update Maintenance Entry summary: Update Maintenance Entry
tags: tags:
- Maintenance - Maintenance
/v1/items/export:
get:
responses:
"200":
description: text/csv
schema:
type: string
security:
- Bearer: []
summary: Export Items
tags:
- Items
/v1/items/fields: /v1/items/fields:
get: get:
produces: produces:
@ -1267,7 +1122,7 @@ paths:
type: array type: array
security: security:
- Bearer: [] - Bearer: []
summary: Get All Custom Field Names summary: imports items into the database
tags: tags:
- Items - Items
/v1/items/fields/values: /v1/items/fields/values:
@ -1283,7 +1138,7 @@ paths:
type: array type: array
security: security:
- Bearer: [] - Bearer: []
summary: Get All Custom Field Values summary: imports items into the database
tags: tags:
- Items - Items
/v1/items/import: /v1/items/import:
@ -1301,7 +1156,7 @@ paths:
description: No Content description: No Content
security: security:
- Bearer: [] - Bearer: []
summary: Import Items summary: imports items into the database
tags: tags:
- Items - Items
/v1/labels: /v1/labels:
@ -1312,9 +1167,14 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
allOf:
- $ref: '#/definitions/server.Results'
- properties:
items:
items: items:
$ref: '#/definitions/repo.LabelOut' $ref: '#/definitions/repo.LabelOut'
type: array type: array
type: object
security: security:
- Bearer: [] - Bearer: []
summary: Get All Labels summary: Get All Labels
@ -1337,7 +1197,7 @@ paths:
$ref: '#/definitions/repo.LabelSummary' $ref: '#/definitions/repo.LabelSummary'
security: security:
- Bearer: [] - Bearer: []
summary: Create Label summary: Create a new label
tags: tags:
- Labels - Labels
/v1/labels/{id}: /v1/labels/{id}:
@ -1355,7 +1215,7 @@ paths:
description: No Content description: No Content
security: security:
- Bearer: [] - Bearer: []
summary: Delete Label summary: deletes a label
tags: tags:
- Labels - Labels
get: get:
@ -1374,7 +1234,7 @@ paths:
$ref: '#/definitions/repo.LabelOut' $ref: '#/definitions/repo.LabelOut'
security: security:
- Bearer: [] - Bearer: []
summary: Get Label summary: Gets a label and fields
tags: tags:
- Labels - Labels
put: put:
@ -1393,7 +1253,7 @@ paths:
$ref: '#/definitions/repo.LabelOut' $ref: '#/definitions/repo.LabelOut'
security: security:
- Bearer: [] - Bearer: []
summary: Update Label summary: updates a label
tags: tags:
- Labels - Labels
/v1/locations: /v1/locations:
@ -1409,9 +1269,14 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
allOf:
- $ref: '#/definitions/server.Results'
- properties:
items:
items: items:
$ref: '#/definitions/repo.LocationOutCount' $ref: '#/definitions/repo.LocationOutCount'
type: array type: array
type: object
security: security:
- Bearer: [] - Bearer: []
summary: Get All Locations summary: Get All Locations
@ -1434,7 +1299,7 @@ paths:
$ref: '#/definitions/repo.LocationSummary' $ref: '#/definitions/repo.LocationSummary'
security: security:
- Bearer: [] - Bearer: []
summary: Create Location summary: Create a new location
tags: tags:
- Locations - Locations
/v1/locations/{id}: /v1/locations/{id}:
@ -1452,7 +1317,7 @@ paths:
description: No Content description: No Content
security: security:
- Bearer: [] - Bearer: []
summary: Delete Location summary: deletes a location
tags: tags:
- Locations - Locations
get: get:
@ -1471,7 +1336,7 @@ paths:
$ref: '#/definitions/repo.LocationOut' $ref: '#/definitions/repo.LocationOut'
security: security:
- Bearer: [] - Bearer: []
summary: Get Location summary: Gets a location and fields
tags: tags:
- Locations - Locations
put: put:
@ -1496,7 +1361,7 @@ paths:
$ref: '#/definitions/repo.LocationOut' $ref: '#/definitions/repo.LocationOut'
security: security:
- Bearer: [] - Bearer: []
summary: Update Location summary: updates a location
tags: tags:
- Locations - Locations
/v1/locations/tree: /v1/locations/tree:
@ -1512,112 +1377,19 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
allOf:
- $ref: '#/definitions/server.Results'
- properties:
items:
items: items:
$ref: '#/definitions/repo.TreeItem' $ref: '#/definitions/repo.TreeItem'
type: array type: array
type: object
security: security:
- Bearer: [] - Bearer: []
summary: Get Locations Tree summary: Get All Locations
tags: tags:
- Locations - Locations
/v1/notifiers:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.NotifierOut'
type: array
security:
- Bearer: []
summary: Get Notifiers
tags:
- Notifiers
post:
parameters:
- description: Notifier Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.NotifierCreate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/repo.NotifierOut'
security:
- Bearer: []
summary: Create Notifier
tags:
- Notifiers
/v1/notifiers/{id}:
delete:
parameters:
- description: Notifier ID
in: path
name: id
required: true
type: string
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Delete a Notifier
tags:
- Notifiers
put:
parameters:
- description: Notifier ID
in: path
name: id
required: true
type: string
- description: Notifier Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/repo.NotifierUpdate'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/repo.NotifierOut'
security:
- Bearer: []
summary: Update Notifier
tags:
- Notifiers
/v1/notifiers/test:
post:
parameters:
- description: Notifier ID
in: path
name: id
required: true
type: string
- description: URL
in: query
name: url
required: true
type: string
produces:
- application/json
responses:
"204":
description: No Content
security:
- Bearer: []
summary: Test Notifier
tags:
- Notifiers
/v1/qrcode: /v1/qrcode:
get: get:
parameters: parameters:
@ -1634,7 +1406,7 @@ paths:
type: string type: string
security: security:
- Bearer: [] - Bearer: []
summary: Create QR Code summary: Encode data into QRCode
tags: tags:
- Items - Items
/v1/reporting/bill-of-materials: /v1/reporting/bill-of-materials:
@ -1648,7 +1420,7 @@ paths:
type: string type: string
security: security:
- Bearer: [] - Bearer: []
summary: Export Bill of Materials summary: Generates a Bill of Materials CSV
tags: tags:
- Reporting - Reporting
/v1/status: /v1/status:
@ -1660,7 +1432,7 @@ paths:
description: OK description: OK
schema: schema:
$ref: '#/definitions/v1.ApiSummary' $ref: '#/definitions/v1.ApiSummary'
summary: Application Info summary: Retrieves the basic information about the API
tags: tags:
- Base - Base
/v1/users/change-password: /v1/users/change-password:
@ -1677,7 +1449,7 @@ paths:
description: No Content description: No Content
security: security:
- Bearer: [] - Bearer: []
summary: Change Password summary: Updates the users password
tags: tags:
- User - User
/v1/users/login: /v1/users/login:
@ -1696,12 +1468,6 @@ paths:
in: formData in: formData
name: password name: password
type: string type: string
- description: Login Data
in: body
name: payload
required: true
schema:
$ref: '#/definitions/v1.LoginForm'
produces: produces:
- application/json - application/json
responses: responses:
@ -1749,7 +1515,7 @@ paths:
responses: responses:
"204": "204":
description: No Content description: No Content
summary: Register New User summary: Get the current user
tags: tags:
- User - User
/v1/users/self: /v1/users/self:
@ -1761,7 +1527,7 @@ paths:
description: No Content description: No Content
security: security:
- Bearer: [] - Bearer: []
summary: Delete Account summary: Deletes the user account
tags: tags:
- User - User
get: get:
@ -1772,14 +1538,14 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/v1.Wrapped' - $ref: '#/definitions/server.Result'
- properties: - properties:
item: item:
$ref: '#/definitions/repo.UserOut' $ref: '#/definitions/repo.UserOut'
type: object type: object
security: security:
- Bearer: [] - Bearer: []
summary: Get User Self summary: Get the current user
tags: tags:
- User - User
put: put:
@ -1797,14 +1563,14 @@ paths:
description: OK description: OK
schema: schema:
allOf: allOf:
- $ref: '#/definitions/v1.Wrapped' - $ref: '#/definitions/server.Result'
- properties: - properties:
item: item:
$ref: '#/definitions/repo.UserUpdate' $ref: '#/definitions/repo.UserUpdate'
type: object type: object
security: security:
- Bearer: [] - Bearer: []
summary: Update Account summary: Update the current user
tags: tags:
- User - User
securityDefinitions: securityDefinitions:

View file

@ -1,29 +1,23 @@
module github.com/hay-kot/homebox/backend module github.com/hay-kot/homebox/backend
go 1.20 go 1.19
require ( require (
ariga.io/atlas v0.12.0 ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb
entgo.io/ent v0.12.3 entgo.io/ent v0.11.8
github.com/ardanlabs/conf/v3 v3.1.6 github.com/ardanlabs/conf/v3 v3.1.4
github.com/containrrr/shoutrrr v0.8.0 github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/chi/v5 v5.0.10 github.com/go-playground/validator/v10 v10.11.2
github.com/go-playground/validator/v10 v10.15.5 github.com/gocarina/gocsv v0.0.0-20230123225133-763e25b40669
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d github.com/google/uuid v1.3.0
github.com/google/uuid v1.3.1 github.com/mattn/go-sqlite3 v1.14.16
github.com/gorilla/schema v1.2.0 github.com/rs/zerolog v1.29.0
github.com/hay-kot/httpkit v0.0.3 github.com/stretchr/testify v1.8.1
github.com/mattn/go-sqlite3 v1.14.17 github.com/swaggo/http-swagger v1.3.3
github.com/olahol/melody v1.1.4 github.com/swaggo/swag v1.8.10
github.com/pkg/errors v0.9.1 github.com/yeqown/go-qrcode/v2 v2.2.1
github.com/rs/zerolog v1.31.0 github.com/yeqown/go-qrcode/writer/standard v1.2.1
github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.6.0
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.2
github.com/yeqown/go-qrcode/v2 v2.2.2
github.com/yeqown/go-qrcode/writer/standard v1.2.2
golang.org/x/crypto v0.14.0
modernc.org/sqlite v1.26.0
) )
require ( require (
@ -31,47 +25,33 @@ require (
github.com/agext/levenshtein v1.2.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fogleman/gg v1.3.0 // indirect github.com/fogleman/gg v1.3.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/swag v0.22.4 // indirect github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl/v2 v2.15.0 // indirect
github.com/hashicorp/hcl/v2 v2.17.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/swaggo/files v1.0.0 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect
github.com/zclconf/go-cty v1.13.2 // indirect github.com/zclconf/go-cty v1.12.1 // indirect
golang.org/x/image v0.9.0 // indirect golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
golang.org/x/mod v0.12.0 // indirect golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.12.0 // indirect golang.org/x/net v0.6.0 // indirect
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.11.0 // indirect golang.org/x/tools v0.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.14 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect
) )

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
Import Ref,Location,Labels,Quantity,Name,Description,Insured,Serial Number,Mode Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased Time,Lifetime Warranty,Warranty Expires,Warranty Details,Sold To,Sold Price,Sold Time,Sold Notes
A,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,Description 1,TRUE,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,10/13/2021,,,,10/13/2021,
B,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,Description 2,FALSE,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,10/15/2021,,,,10/15/2021,
C,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,Description 3,TRUE,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,10/13/2021,,,,10/13/2021,
D,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,Description 4,FALSE,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,10/21/2020,,,,10/21/2020,
E,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,Description 5,TRUE,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,10/14/2020,,,,10/14/2020,
F,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,Description 6,FALSE,,39351,Honeywell,,Amazon,65.98,09/30/2020,,09/30/2020,,,,09/30/2020,
1 Import Ref Location Labels Quantity Name Description Insured Serial Number Mode Number Manufacturer Notes Purchase From Purchased Price Purchased Time Lifetime Warranty Warranty Expires Warranty Details Sold To Sold Price Sold Time Sold Notes
2 A Garage IOT;Home Assistant; Z-Wave 1 Zooz Universal Relay ZEN17 Description 1 TRUE ZEN17 Zooz Amazon 39.95 10/13/2021 10/13/2021 10/13/2021
3 B Living Room IOT;Home Assistant; Z-Wave 1 Zooz Motion Sensor Description 2 FALSE ZSE18 Zooz Amazon 29.95 10/15/2021 10/15/2021 10/15/2021
4 C Office IOT;Home Assistant; Z-Wave 1 Zooz 110v Power Switch Description 3 TRUE ZEN15 Zooz Amazon 39.95 10/13/2021 10/13/2021 10/13/2021
5 D Downstairs IOT;Home Assistant; Z-Wave 1 Ecolink Z-Wave PIR Motion Sensor Description 4 FALSE PIRZWAVE2.5-ECO Ecolink Amazon 35.58 10/21/2020 10/21/2020 10/21/2020
6 E Entry IOT;Home Assistant; Z-Wave 1 Yale Security Touchscreen Deadbolt Description 5 TRUE YRD226ZW2619 Yale Amazon 120.39 10/14/2020 10/14/2020 10/14/2020
7 F Kitchen IOT;Home Assistant; Z-Wave 1 Smart Rocker Light Dimmer Description 6 FALSE 39351 Honeywell Amazon 65.98 09/30/2020 09/30/2020 09/30/2020

View file

@ -0,0 +1,7 @@
Import Ref Location Labels Quantity Name Description Insured Serial Number Mode Number Manufacturer Notes Purchase From Purchased Price Purchased Time Lifetime Warranty Warranty Expires Warranty Details Sold To Sold Price Sold Time Sold Notes
A Garage IOT;Home Assistant; Z-Wave 1 Zooz Universal Relay ZEN17 Description 1 TRUE ZEN17 Zooz Amazon 39.95 10/13/2021 10/13/2021 10/13/2021
B Living Room IOT;Home Assistant; Z-Wave 1 Zooz Motion Sensor Description 2 FALSE ZSE18 Zooz Amazon 29.95 10/15/2021 10/15/2021 10/15/2021
C Office IOT;Home Assistant; Z-Wave 1 Zooz 110v Power Switch Description 3 TRUE ZEN15 Zooz Amazon 39.95 10/13/2021 10/13/2021 10/13/2021
D Downstairs IOT;Home Assistant; Z-Wave 1 Ecolink Z-Wave PIR Motion Sensor Description 4 FALSE PIRZWAVE2.5-ECO Ecolink Amazon 35.58 10/21/2020 10/21/2020 10/21/2020
E Entry IOT;Home Assistant; Z-Wave 1 Yale Security Touchscreen Deadbolt Description 5 TRUE YRD226ZW2619 Yale Amazon 120.39 10/14/2020 10/14/2020 10/14/2020
F Kitchen IOT;Home Assistant; Z-Wave 1 Smart Rocker Light Dimmer Description 6 FALSE 39351 Honeywell Amazon 65.98 09/30/2020 09/30/2020 09/30/2020
1 Import Ref Location Labels Quantity Name Description Insured Serial Number Mode Number Manufacturer Notes Purchase From Purchased Price Purchased Time Lifetime Warranty Warranty Expires Warranty Details Sold To Sold Price Sold Time Sold Notes
2 A Garage IOT;Home Assistant; Z-Wave 1 Zooz Universal Relay ZEN17 Description 1 TRUE ZEN17 Zooz Amazon 39.95 10/13/2021 10/13/2021 10/13/2021
3 B Living Room IOT;Home Assistant; Z-Wave 1 Zooz Motion Sensor Description 2 FALSE ZSE18 Zooz Amazon 29.95 10/15/2021 10/15/2021 10/15/2021
4 C Office IOT;Home Assistant; Z-Wave 1 Zooz 110v Power Switch Description 3 TRUE ZEN15 Zooz Amazon 39.95 10/13/2021 10/13/2021 10/13/2021
5 D Downstairs IOT;Home Assistant; Z-Wave 1 Ecolink Z-Wave PIR Motion Sensor Description 4 FALSE PIRZWAVE2.5-ECO Ecolink Amazon 35.58 10/21/2020 10/21/2020 10/21/2020
6 E Entry IOT;Home Assistant; Z-Wave 1 Yale Security Touchscreen Deadbolt Description 5 TRUE YRD226ZW2619 Yale Amazon 120.39 10/14/2020 10/14/2020 10/14/2020
7 F Kitchen IOT;Home Assistant; Z-Wave 1 Smart Rocker Light Dimmer Description 6 FALSE 39351 Honeywell Amazon 65.98 09/30/2020 09/30/2020 09/30/2020

View file

@ -1,14 +1,16 @@
package services package services
import ( import (
"github.com/hay-kot/homebox/backend/internal/core/services/reporting"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/rs/zerolog/log"
) )
type AllServices struct { type AllServices struct {
User *UserService User *UserService
Group *GroupService Group *GroupService
Items *ItemService Items *ItemService
BackgroundService *BackgroundService Reporting *reporting.ReportingService
} }
type OptionsFunc func(*options) type OptionsFunc func(*options)
@ -43,6 +45,7 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
repo: repos, repo: repos,
autoIncrementAssetID: options.autoIncrementAssetID, autoIncrementAssetID: options.autoIncrementAssetID,
}, },
BackgroundService: &BackgroundService{repos}, // TODO: don't use global logger
Reporting: reporting.NewReportingService(repos, &log.Logger),
} }
} }

View file

@ -3,10 +3,11 @@ package services
import ( import (
"context" "context"
"log" "log"
"math/rand"
"os" "os"
"testing" "testing"
"time"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/pkgs/faker" "github.com/hay-kot/homebox/backend/pkgs/faker"
@ -15,7 +16,6 @@ import (
var ( var (
fk = faker.NewFaker() fk = faker.NewFaker()
tbus = eventbus.New()
tCtx = Context{} tCtx = Context{}
tClient *ent.Client tClient *ent.Client
@ -49,6 +49,8 @@ func bootstrap() {
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
rand.Seed(int64(time.Now().Unix()))
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil { if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err) log.Fatalf("failed opening connection to sqlite: %v", err)
@ -60,7 +62,7 @@ func TestMain(m *testing.M) {
} }
tClient = client tClient = client
tRepos = repo.New(tClient, tbus, os.TempDir()+"/homebox") tRepos = repo.New(tClient, os.TempDir()+"/homebox")
tSvc = New(tRepos) tSvc = New(tRepos)
defer client.Close() defer client.Close()

View file

@ -1,5 +0,0 @@
HB.location,HB.name,HB.quantity,HB.description,HB.field.Custom Field 1,HB.field.Custom Field 2,HB.field.Custom Field 3
loc,Item 1,1,Description 1,Value 1[1],Value 1[2],Value 1[3]
loc,Item 2,2,Description 2,Value 2[1],Value 2[2],Value 2[3]
loc,Item 3,3,Description 3,Value 3[1],Value 3[2],Value 3[3]
1 HB.location HB.name HB.quantity HB.description HB.field.Custom Field 1 HB.field.Custom Field 2 HB.field.Custom Field 3
2 loc Item 1 1 Description 1 Value 1[1] Value 1[2] Value 1[3]
3 loc Item 2 2 Description 2 Value 2[1] Value 2[2] Value 2[3]
4 loc Item 3 3 Description 3 Value 3[1] Value 3[2] Value 3[3]

View file

@ -1,4 +0,0 @@
HB.location,HB.name,HB.quantity,HB.description
loc,Item 1,1,Description 1
loc,Item 2,2,Description 2
loc,Item 3,3,Description 3
1 HB.location HB.name HB.quantity HB.description
2 loc Item 1 1 Description 1
3 loc Item 2 2 Description 2
4 loc Item 3 3 Description 3

View file

@ -1,4 +0,0 @@
HB.name,HB.asset_id,HB.location,HB.labels
Item 1,1,Path / To / Location 1,L1 ; L2 ; L3
Item 2,000-002,Path /To/ Location 2,L1;L2;L3
Item 3,1000-003,Path / To /Location 3 , L1;L2; L3
1 HB.name HB.asset_id HB.location HB.labels
2 Item 1 1 Path / To / Location 1 L1 ; L2 ; L3
3 Item 2 000-002 Path /To/ Location 2 L1;L2;L3
4 Item 3 1000-003 Path / To /Location 3 L1;L2; L3

View file

@ -1,42 +0,0 @@
package reporting
import (
"github.com/gocarina/gocsv"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
)
// =================================================================================================
type BillOfMaterialsEntry struct {
PurchaseDate types.Date `csv:"Purchase Date"`
Name string `csv:"Name"`
Description string `csv:"Description"`
Manufacturer string `csv:"Manufacturer"`
SerialNumber string `csv:"Serial Number"`
ModelNumber string `csv:"Model Number"`
Quantity int `csv:"Quantity"`
Price float64 `csv:"Price"`
TotalPrice float64 `csv:"Total Price"`
}
// BillOfMaterialsTSV returns a byte slice of the Bill of Materials for a given GID in TSV format
// See BillOfMaterialsEntry for the format of the output
func BillOfMaterialsTSV(entities []repo.ItemOut) ([]byte, error) {
bomEntries := make([]BillOfMaterialsEntry, len(entities))
for i, entity := range entities {
bomEntries[i] = BillOfMaterialsEntry{
PurchaseDate: entity.PurchaseTime,
Name: entity.Name,
Description: entity.Description,
Manufacturer: entity.Manufacturer,
SerialNumber: entity.SerialNumber,
ModelNumber: entity.ModelNumber,
Quantity: entity.Quantity,
Price: entity.PurchasePrice,
TotalPrice: entity.PurchasePrice * float64(entity.Quantity),
}
}
return gocsv.MarshalBytes(&bomEntries)
}

View file

@ -1,85 +0,0 @@
// / Package eventbus provides an interface for event bus.
package eventbus
import (
"sync"
"github.com/google/uuid"
)
type Event string
const (
EventLabelMutation Event = "label.mutation"
EventLocationMutation Event = "location.mutation"
EventItemMutation Event = "item.mutation"
)
type GroupMutationEvent struct {
GID uuid.UUID
}
type eventData struct {
event Event
data any
}
type EventBus struct {
started bool
ch chan eventData
mu sync.RWMutex
subscribers map[Event][]func(any)
}
func New() *EventBus {
return &EventBus{
ch: make(chan eventData, 10),
subscribers: map[Event][]func(any){
EventLabelMutation: {},
EventLocationMutation: {},
EventItemMutation: {},
},
}
}
func (e *EventBus) Run() {
if e.started {
panic("event bus already started")
}
e.started = true
for event := range e.ch {
e.mu.RLock()
arr, ok := e.subscribers[event.event]
e.mu.RUnlock()
if !ok {
continue
}
for _, fn := range arr {
fn(event.data)
}
}
}
func (e *EventBus) Publish(event Event, data any) {
e.ch <- eventData{
event: event,
data: data,
}
}
func (e *EventBus) Subscribe(event Event, fn func(any)) {
e.mu.Lock()
defer e.mu.Unlock()
arr, ok := e.subscribers[event]
if !ok {
panic("event not found")
}
e.subscribers[event] = append(arr, fn)
}

View file

@ -1,93 +0,0 @@
package reporting
import (
"bytes"
"encoding/csv"
"errors"
"io"
"strings"
)
var (
ErrNoHomeboxHeaders = errors.New("no headers found")
ErrMissingRequiredHeaders = errors.New("missing required headers `HB.location` or `HB.name`")
)
// determineSeparator determines the separator used in the CSV file
// It returns the separator as a rune and an error if it could not be determined
//
// It is assumed that the first row is the header row and that the separator is the same
// for all rows.
//
// Supported separators are `,` and `\t`
func determineSeparator(data []byte) (rune, error) {
// First row
firstRow := bytes.Split(data, []byte("\n"))[0]
// find first comma or /t
comma := bytes.IndexByte(firstRow, ',')
tab := bytes.IndexByte(firstRow, '\t')
switch {
case comma == -1 && tab == -1:
return 0, errors.New("could not determine separator")
case tab > comma:
return '\t', nil
default:
return ',', nil
}
}
// readRawCsv reads a CSV file and returns the raw data as a 2D string array
// It determines the separator used in the CSV file and returns an error if
// it could not be determined
func readRawCsv(r io.Reader) ([][]string, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
reader := csv.NewReader(bytes.NewReader(data))
// Determine separator
sep, err := determineSeparator(data)
if err != nil {
return nil, err
}
reader.Comma = sep
return reader.ReadAll()
}
// parseHeaders parses the homebox headers from the CSV file and returns a map of the headers
// and their column index as well as a list of the field headers (HB.field.*) in the order
// they appear in the CSV file
//
// It returns an error if no homebox headers are found
func parseHeaders(headers []string) (hbHeaders map[string]int, fieldHeaders []string, err error) {
hbHeaders = map[string]int{} // initialize map
for col, h := range headers {
if strings.HasPrefix(h, "HB.field.") {
fieldHeaders = append(fieldHeaders, h)
}
if strings.HasPrefix(h, "HB.") {
hbHeaders[h] = col
}
}
required := []string{"HB.location", "HB.name"}
for _, h := range required {
if _, ok := hbHeaders[h]; !ok {
return nil, nil, ErrMissingRequiredHeaders
}
}
if len(hbHeaders) == 0 {
return nil, nil, ErrNoHomeboxHeaders
}
return hbHeaders, fieldHeaders, nil
}

View file

@ -1,95 +0,0 @@
package reporting
import (
"strings"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
)
type ExportItemFields struct {
Name string
Value string
}
type ExportTSVRow struct {
ImportRef string `csv:"HB.import_ref"`
Location LocationString `csv:"HB.location"`
LabelStr LabelString `csv:"HB.labels"`
AssetID repo.AssetID `csv:"HB.asset_id"`
Archived bool `csv:"HB.archived"`
Name string `csv:"HB.name"`
Quantity int `csv:"HB.quantity"`
Description string `csv:"HB.description"`
Insured bool `csv:"HB.insured"`
Notes string `csv:"HB.notes"`
PurchasePrice float64 `csv:"HB.purchase_price"`
PurchaseFrom string `csv:"HB.purchase_from"`
PurchaseTime types.Date `csv:"HB.purchase_time"`
Manufacturer string `csv:"HB.manufacturer"`
ModelNumber string `csv:"HB.model_number"`
SerialNumber string `csv:"HB.serial_number"`
LifetimeWarranty bool `csv:"HB.lifetime_warranty"`
WarrantyExpires types.Date `csv:"HB.warranty_expires"`
WarrantyDetails string `csv:"HB.warranty_details"`
SoldTo string `csv:"HB.sold_to"`
SoldPrice float64 `csv:"HB.sold_price"`
SoldTime types.Date `csv:"HB.sold_time"`
SoldNotes string `csv:"HB.sold_notes"`
Fields []ExportItemFields `csv:"-"`
}
// ============================================================================
// LabelString is a string slice that is used to represent a list of labels.
//
// For example, a list of labels "Important; Work" would be represented as a
// LabelString with the following values:
//
// LabelString{"Important", "Work"}
type LabelString []string
func parseLabelString(s string) LabelString {
v, _ := parseSeparatedString(s, ";")
return v
}
func (ls LabelString) String() string {
return strings.Join(ls, "; ")
}
// ============================================================================
// LocationString is a string slice that is used to represent a location
// hierarchy.
//
// For example, a location hierarchy of "Home / Bedroom / Desk" would be
// represented as a LocationString with the following values:
//
// LocationString{"Home", "Bedroom", "Desk"}
type LocationString []string
func parseLocationString(s string) LocationString {
v, _ := parseSeparatedString(s, "/")
return v
}
func (csf LocationString) String() string {
return strings.Join(csf, " / ")
}
func fromPathSlice(s []repo.LocationPath) LocationString {
v := make(LocationString, len(s))
for i := range s {
v[i] = s[i].Name
}
return v
}

View file

@ -1,322 +0,0 @@
package reporting
import (
"context"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
"github.com/rs/zerolog/log"
)
// IOSheet is the representation of a CSV/TSV sheet that is used for importing/exporting
// items from homebox. It is used to read/write the data from/to a CSV/TSV file given
// the standard format of the file.
//
// See ExportTSVRow for the format of the data in the sheet.
type IOSheet struct {
headers []string
custom []int
index map[string]int
Rows []ExportTSVRow
}
func (s *IOSheet) indexHeaders() {
s.index = make(map[string]int)
for i, h := range s.headers {
if strings.HasPrefix(h, "HB.field") {
s.custom = append(s.custom, i)
}
if strings.HasPrefix(h, "HB.") {
s.index[h] = i
}
}
}
func (s *IOSheet) GetColumn(str string) (col int, ok bool) {
if s.index == nil {
s.indexHeaders()
}
col, ok = s.index[str]
return
}
// Read reads a CSV/TSV and populates the "Rows" field with the data from the sheet
// Custom Fields are supported via the `HB.field.*` headers. The `HB.field.*` the "Name"
// of the field is the part after the `HB.field.` prefix. Additionally, Custom Fields with
// no value are excluded from the row.Fields slice, this includes empty strings.
//
// Note That
// - the first row is assumed to be the header
// - at least 1 row of data is required
// - rows and columns must be rectangular (i.e. all rows must have the same number of columns)
func (s *IOSheet) Read(data io.Reader) error {
sheet, err := readRawCsv(data)
if err != nil {
return err
}
if len(sheet) < 2 {
return fmt.Errorf("sheet must have at least 1 row of data (header + 1)")
}
s.headers = sheet[0]
s.Rows = make([]ExportTSVRow, len(sheet)-1)
for i, row := range sheet[1:] {
if len(row) != len(s.headers) {
return fmt.Errorf("row has %d columns, expected %d", len(row), len(s.headers))
}
rowData := ExportTSVRow{}
st := reflect.TypeOf(ExportTSVRow{})
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
tag := field.Tag.Get("csv")
if tag == "" || tag == "-" {
continue
}
col, ok := s.GetColumn(tag)
if !ok {
continue
}
val := row[col]
var v interface{}
switch field.Type {
case reflect.TypeOf(""):
v = val
case reflect.TypeOf(int(0)):
v = parseInt(val)
case reflect.TypeOf(bool(false)):
v = parseBool(val)
case reflect.TypeOf(float64(0)):
v = parseFloat(val)
// Custom Types
case reflect.TypeOf(types.Date{}):
v = types.DateFromString(val)
case reflect.TypeOf(repo.AssetID(0)):
v, _ = repo.ParseAssetID(val)
case reflect.TypeOf(LocationString{}):
v = parseLocationString(val)
case reflect.TypeOf(LabelString{}):
v = parseLabelString(val)
}
log.Debug().
Str("tag", tag).
Interface("val", v).
Str("type", fmt.Sprintf("%T", v)).
Msg("parsed value")
// Nil values are not allowed at the moment. This may change.
if v == nil {
return fmt.Errorf("could not convert %q to %s", val, field.Type)
}
ptrField := reflect.ValueOf(&rowData).Elem().Field(i)
ptrField.Set(reflect.ValueOf(v))
}
for _, col := range s.custom {
colName := strings.TrimPrefix(s.headers[col], "HB.field.")
customVal := row[col]
if customVal == "" {
continue
}
rowData.Fields = append(rowData.Fields, ExportItemFields{
Name: colName,
Value: customVal,
})
}
s.Rows[i] = rowData
}
return nil
}
// Write writes the sheet to a writer.
func (s *IOSheet) ReadItems(ctx context.Context, items []repo.ItemOut, GID uuid.UUID, repos *repo.AllRepos) error {
s.Rows = make([]ExportTSVRow, len(items))
extraHeaders := map[string]struct{}{}
for i := range items {
item := items[i]
// TODO: Support fetching nested locations
locId := item.Location.ID
locPaths, err := repos.Locations.PathForLoc(context.Background(), GID, locId)
if err != nil {
log.Error().Err(err).Msg("could not get location path")
return err
}
locString := fromPathSlice(locPaths)
labelString := make([]string, len(item.Labels))
for i, l := range item.Labels {
labelString[i] = l.Name
}
customFields := make([]ExportItemFields, len(item.Fields))
for i, f := range item.Fields {
extraHeaders[f.Name] = struct{}{}
customFields[i] = ExportItemFields{
Name: f.Name,
Value: f.TextValue,
}
}
s.Rows[i] = ExportTSVRow{
// fill struct
Location: locString,
LabelStr: labelString,
ImportRef: item.ImportRef,
AssetID: item.AssetID,
Name: item.Name,
Quantity: item.Quantity,
Description: item.Description,
Insured: item.Insured,
Archived: item.Archived,
PurchasePrice: item.PurchasePrice,
PurchaseFrom: item.PurchaseFrom,
PurchaseTime: item.PurchaseTime,
Manufacturer: item.Manufacturer,
ModelNumber: item.ModelNumber,
SerialNumber: item.SerialNumber,
LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: item.WarrantyExpires,
WarrantyDetails: item.WarrantyDetails,
SoldTo: item.SoldTo,
SoldTime: item.SoldTime,
SoldPrice: item.SoldPrice,
SoldNotes: item.SoldNotes,
Fields: customFields,
}
}
// Extract and sort additional headers for deterministic output
customHeaders := make([]string, 0, len(extraHeaders))
for k := range extraHeaders {
customHeaders = append(customHeaders, k)
}
sort.Strings(customHeaders)
st := reflect.TypeOf(ExportTSVRow{})
// Write headers
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
tag := field.Tag.Get("csv")
if tag == "" || tag == "-" {
continue
}
s.headers = append(s.headers, tag)
}
for _, h := range customHeaders {
s.headers = append(s.headers, "HB.field."+h)
}
return nil
}
// Writes the current sheet to a writer in TSV format.
func (s *IOSheet) TSV() ([][]string, error) {
memcsv := make([][]string, len(s.Rows)+1)
memcsv[0] = s.headers
// use struct tags in rows to dertmine column order
for i, row := range s.Rows {
rowIdx := i + 1
memcsv[rowIdx] = make([]string, len(s.headers))
st := reflect.TypeOf(row)
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
tag := field.Tag.Get("csv")
if tag == "" || tag == "-" {
continue
}
col, ok := s.GetColumn(tag)
if !ok {
continue
}
val := reflect.ValueOf(row).Field(i)
var v string
switch field.Type {
case reflect.TypeOf(""):
v = val.String()
case reflect.TypeOf(int(0)):
v = strconv.Itoa(int(val.Int()))
case reflect.TypeOf(bool(false)):
v = strconv.FormatBool(val.Bool())
case reflect.TypeOf(float64(0)):
v = strconv.FormatFloat(val.Float(), 'f', -1, 64)
// Custom Types
case reflect.TypeOf(types.Date{}):
v = val.Interface().(types.Date).String()
case reflect.TypeOf(repo.AssetID(0)):
v = val.Interface().(repo.AssetID).String()
case reflect.TypeOf(LocationString{}):
v = val.Interface().(LocationString).String()
case reflect.TypeOf(LabelString{}):
v = val.Interface().(LabelString).String()
default:
log.Debug().Str("type", field.Type.String()).Msg("unknown type")
}
memcsv[rowIdx][col] = v
}
for _, f := range row.Fields {
col, ok := s.GetColumn("HB.field." + f.Name)
if !ok {
continue
}
memcsv[i+1][col] = f.Value
}
}
return memcsv, nil
}

View file

@ -1,220 +0,0 @@
package reporting
import (
"bytes"
"reflect"
"testing"
_ "embed"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/stretchr/testify/assert"
)
var (
//go:embed .testdata/import/minimal.csv
minimalImportCSV []byte
//go:embed .testdata/import/fields.csv
customFieldImportCSV []byte
//go:embed .testdata/import/types.csv
customTypesImportCSV []byte
)
func TestSheet_Read(t *testing.T) {
tests := []struct {
name string
data []byte
want []ExportTSVRow
wantErr bool
}{
{
name: "minimal import",
data: minimalImportCSV,
want: []ExportTSVRow{
{Location: LocationString{"loc"}, Name: "Item 1", Quantity: 1, Description: "Description 1"},
{Location: LocationString{"loc"}, Name: "Item 2", Quantity: 2, Description: "Description 2"},
{Location: LocationString{"loc"}, Name: "Item 3", Quantity: 3, Description: "Description 3"},
},
},
{
name: "custom field import",
data: customFieldImportCSV,
want: []ExportTSVRow{
{
Location: LocationString{"loc"}, Name: "Item 1", Quantity: 1, Description: "Description 1",
Fields: []ExportItemFields{
{Name: "Custom Field 1", Value: "Value 1[1]"},
{Name: "Custom Field 2", Value: "Value 1[2]"},
{Name: "Custom Field 3", Value: "Value 1[3]"},
},
},
{
Location: LocationString{"loc"}, Name: "Item 2", Quantity: 2, Description: "Description 2",
Fields: []ExportItemFields{
{Name: "Custom Field 1", Value: "Value 2[1]"},
{Name: "Custom Field 2", Value: "Value 2[2]"},
{Name: "Custom Field 3", Value: "Value 2[3]"},
},
},
{
Location: LocationString{"loc"}, Name: "Item 3", Quantity: 3, Description: "Description 3",
Fields: []ExportItemFields{
{Name: "Custom Field 1", Value: "Value 3[1]"},
{Name: "Custom Field 2", Value: "Value 3[2]"},
{Name: "Custom Field 3", Value: "Value 3[3]"},
},
},
},
},
{
name: "custom types import",
data: customTypesImportCSV,
want: []ExportTSVRow{
{
Name: "Item 1",
AssetID: repo.AssetID(1),
Location: LocationString{"Path", "To", "Location 1"},
LabelStr: LabelString{"L1", "L2", "L3"},
},
{
Name: "Item 2",
AssetID: repo.AssetID(2),
Location: LocationString{"Path", "To", "Location 2"},
LabelStr: LabelString{"L1", "L2", "L3"},
},
{
Name: "Item 3",
AssetID: repo.AssetID(1000003),
Location: LocationString{"Path", "To", "Location 3"},
LabelStr: LabelString{"L1", "L2", "L3"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := bytes.NewReader(tt.data)
sheet := &IOSheet{}
err := sheet.Read(reader)
switch {
case tt.wantErr:
assert.Error(t, err)
default:
assert.NoError(t, err)
assert.ElementsMatch(t, tt.want, sheet.Rows)
}
})
}
}
func Test_parseHeaders(t *testing.T) {
tests := []struct {
name string
rawHeaders []string
wantHbHeaders map[string]int
wantFieldHeaders []string
wantErr bool
}{
{
name: "no hombox headers",
rawHeaders: []string{"Header 1", "Header 2", "Header 3"},
wantHbHeaders: nil,
wantFieldHeaders: nil,
wantErr: true,
},
{
name: "field headers only",
rawHeaders: []string{"HB.location", "HB.name", "HB.field.1", "HB.field.2", "HB.field.3"},
wantHbHeaders: map[string]int{
"HB.location": 0,
"HB.name": 1,
"HB.field.1": 2,
"HB.field.2": 3,
"HB.field.3": 4,
},
wantFieldHeaders: []string{"HB.field.1", "HB.field.2", "HB.field.3"},
wantErr: false,
},
{
name: "mixed headers",
rawHeaders: []string{"Header 1", "HB.name", "Header 2", "HB.field.2", "Header 3", "HB.field.3", "HB.location"},
wantHbHeaders: map[string]int{
"HB.name": 1,
"HB.field.2": 3,
"HB.field.3": 5,
"HB.location": 6,
},
wantFieldHeaders: []string{"HB.field.2", "HB.field.3"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotHbHeaders, gotFieldHeaders, err := parseHeaders(tt.rawHeaders)
if (err != nil) != tt.wantErr {
t.Errorf("parseHeaders() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotHbHeaders, tt.wantHbHeaders) {
t.Errorf("parseHeaders() gotHbHeaders = %v, want %v", gotHbHeaders, tt.wantHbHeaders)
}
if !reflect.DeepEqual(gotFieldHeaders, tt.wantFieldHeaders) {
t.Errorf("parseHeaders() gotFieldHeaders = %v, want %v", gotFieldHeaders, tt.wantFieldHeaders)
}
})
}
}
func Test_determineSeparator(t *testing.T) {
type args struct {
data []byte
}
tests := []struct {
name string
args args
want rune
wantErr bool
}{
{
name: "comma",
args: args{
data: []byte("a,b,c"),
},
want: ',',
wantErr: false,
},
{
name: "tab",
args: args{
data: []byte("a\tb\tc"),
},
want: '\t',
wantErr: false,
},
{
name: "invalid",
args: args{
data: []byte("a;b;c"),
},
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := determineSeparator(tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("determineSeparator() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("determineSeparator() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -0,0 +1,85 @@
package reporting
import (
"context"
"encoding/csv"
"io"
"time"
"github.com/gocarina/gocsv"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/rs/zerolog"
)
type ReportingService struct {
repos *repo.AllRepos
l *zerolog.Logger
}
func NewReportingService(repos *repo.AllRepos, l *zerolog.Logger) *ReportingService {
gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter {
writer := csv.NewWriter(out)
writer.Comma = '\t'
return gocsv.NewSafeCSVWriter(writer)
})
return &ReportingService{
repos: repos,
l: l,
}
}
// =================================================================================================
// NullableTime is a custom type that implements the MarshalCSV interface
// to allow for nullable time.Time fields in the CSV output to be empty
// and not "0001-01-01". It also overrides the default CSV output format
type NullableTime time.Time
func (t NullableTime) MarshalCSV() (string, error) {
if time.Time(t).IsZero() {
return "", nil
}
// YYYY-MM-DD
return time.Time(t).Format("2006-01-02"), nil
}
type BillOfMaterialsEntry struct {
PurchaseDate NullableTime `csv:"Purchase Date"`
Name string `csv:"Name"`
Description string `csv:"Description"`
Manufacturer string `csv:"Manufacturer"`
SerialNumber string `csv:"Serial Number"`
ModelNumber string `csv:"Model Number"`
Quantity int `csv:"Quantity"`
Price float64 `csv:"Price"`
TotalPrice float64 `csv:"Total Price"`
}
// BillOfMaterialsTSV returns a byte slice of the Bill of Materials for a given GID in TSV format
// See BillOfMaterialsEntry for the format of the output
func (rs *ReportingService) BillOfMaterialsTSV(ctx context.Context, GID uuid.UUID) ([]byte, error) {
entities, err := rs.repos.Items.GetAll(ctx, GID)
if err != nil {
rs.l.Debug().Err(err).Msg("failed to get all items for BOM Csv Reporting")
return nil, err
}
bomEntries := make([]BillOfMaterialsEntry, len(entities))
for i, entity := range entities {
bomEntries[i] = BillOfMaterialsEntry{
PurchaseDate: NullableTime(entity.PurchaseTime),
Name: entity.Name,
Description: entity.Description,
Manufacturer: entity.Manufacturer,
SerialNumber: entity.SerialNumber,
ModelNumber: entity.ModelNumber,
Quantity: entity.Quantity,
Price: entity.PurchasePrice,
TotalPrice: entity.PurchasePrice * float64(entity.Quantity),
}
}
return gocsv.MarshalBytes(&bomEntries)
}

View file

@ -1,38 +0,0 @@
package reporting
import (
"strconv"
"strings"
)
func parseSeparatedString(s string, sep string) ([]string, error) {
list := strings.Split(s, sep)
csf := make([]string, 0, len(list))
for _, s := range list {
trimmed := strings.TrimSpace(s)
if trimmed != "" {
csf = append(csf, trimmed)
}
}
return csf, nil
}
func parseFloat(s string) float64 {
if s == "" {
return 0
}
f, _ := strconv.ParseFloat(s, 64)
return f
}
func parseBool(s string) bool {
b, _ := strconv.ParseBool(s)
return b
}
func parseInt(s string) int {
i, _ := strconv.Atoi(s)
return i
}

View file

@ -1,65 +0,0 @@
package reporting
import (
"reflect"
"testing"
)
func Test_parseSeparatedString(t *testing.T) {
type args struct {
s string
sep string
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "comma",
args: args{
s: "a,b,c",
sep: ",",
},
want: []string{"a", "b", "c"},
wantErr: false,
},
{
name: "trimmed comma",
args: args{
s: "a, b, c",
sep: ",",
},
want: []string{"a", "b", "c"},
},
{
name: "excessive whitespace",
args: args{
s: " a, b, c ",
sep: ",",
},
want: []string{"a", "b", "c"},
},
{
name: "empty",
args: args{
s: "",
sep: ",",
},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseSeparatedString(tt.args.s, tt.args.sep)
if (err != nil) != tt.wantErr {
t.Errorf("parseSeparatedString() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseSeparatedString() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,81 +0,0 @@
package services
import (
"context"
"strings"
"time"
"github.com/containrrr/shoutrrr"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
"github.com/rs/zerolog/log"
)
type BackgroundService struct {
repos *repo.AllRepos
}
func (svc *BackgroundService) SendNotifiersToday(ctx context.Context) error {
// Get All Groups
groups, err := svc.repos.Groups.GetAllGroups(ctx)
if err != nil {
return err
}
today := types.DateFromTime(time.Now())
for i := range groups {
group := groups[i]
entries, err := svc.repos.MaintEntry.GetScheduled(ctx, group.ID, today)
if err != nil {
return err
}
if len(entries) == 0 {
log.Debug().
Str("group_name", group.Name).
Str("group_id", group.ID.String()).
Msg("No scheduled maintenance for today")
continue
}
notifiers, err := svc.repos.Notifiers.GetByGroup(ctx, group.ID)
if err != nil {
return err
}
urls := make([]string, len(notifiers))
for i := range notifiers {
urls[i] = notifiers[i].URL
}
bldr := strings.Builder{}
bldr.WriteString("Homebox Maintenance for (")
bldr.WriteString(today.String())
bldr.WriteString("):\n")
for i := range entries {
entry := entries[i]
bldr.WriteString(" - ")
bldr.WriteString(entry.Name)
bldr.WriteString("\n")
}
var sendErrs []error
for i := range urls {
err := shoutrrr.Send(urls[i], bldr.String())
if err != nil {
sendErrs = append(sendErrs, err)
}
}
if len(sendErrs) > 0 {
return sendErrs[0]
}
}
return nil
}

View file

@ -3,13 +3,10 @@ package services
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting"
"github.com/hay-kot/homebox/backend/internal/data/repo" "github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/rs/zerolog/log"
) )
var ( var (
@ -40,6 +37,7 @@ func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut,
func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int, error) { func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int, error) {
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, GID) items, err := svc.repo.Items.GetAllZeroAssetID(ctx, GID)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -63,293 +61,190 @@ func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int,
return finished, nil return finished, nil
} }
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]string) (int, error) {
loaded := []csvRow{}
func (svc *ItemService) EnsureImportRef(ctx context.Context, GID uuid.UUID) (int, error) { // Skip first row
ids, err := svc.repo.Items.GetAllZeroImportRef(ctx, GID) for _, row := range data[1:] {
// Skip empty rows
if len(row) == 0 {
continue
}
if len(row) != NumOfCols {
return 0, ErrInvalidCsv
}
r := newCsvRow(row)
loaded = append(loaded, r)
}
// validate rows
var errMap = map[int][]error{}
var hasErr bool
for i, r := range loaded {
errs := r.validate()
if len(errs) > 0 {
hasErr = true
lineNum := i + 2
errMap[lineNum] = errs
}
}
if hasErr {
for lineNum, errs := range errMap {
for _, err := range errs {
log.Error().Err(err).Int("line", lineNum).Msg("csv import error")
}
}
}
// Bootstrap the locations and labels so we can reuse the created IDs for the items
locations := map[string]uuid.UUID{}
existingLocation, err := svc.repo.Locations.GetAll(ctx, GID, repo.LocationQuery{})
if err != nil { if err != nil {
return 0, err return 0, err
} }
for _, loc := range existingLocation {
locations[loc.Name] = loc.ID
}
finished := 0 labels := map[string]uuid.UUID{}
for _, itemID := range ids { existingLabels, err := svc.repo.Labels.GetAll(ctx, GID)
ref := uuid.New().String()[0:8]
err = svc.repo.Items.Patch(ctx, GID, itemID, repo.ItemPatch{ImportRef: &ref})
if err != nil { if err != nil {
return 0, err return 0, err
} }
for _, label := range existingLabels {
finished++ labels[label.Name] = label.ID
} }
return finished, nil for _, row := range loaded {
}
func serializeLocation[T ~[]string](location T) string {
return strings.Join(location, "/")
}
// CsvImport imports items from a CSV file. using the standard defined format.
//
// CsvImport applies the following rules/operations
//
// 1. If the item does not exist, it is created.
// 2. If the item has a ImportRef and it exists it is skipped
// 3. Locations and Labels are created if they do not exist.
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Reader) (int, error) {
sheet := reporting.IOSheet{}
err := sheet.Read(data)
if err != nil {
return 0, err
}
// ========================================
// Labels
labelMap := make(map[string]uuid.UUID)
{
labels, err := svc.repo.Labels.GetAll(ctx, GID)
if err != nil {
return 0, err
}
for _, label := range labels {
labelMap[label.Name] = label.ID
}
}
// ========================================
// Locations // Locations
if _, exists := locations[row.Location]; !exists {
locationMap := make(map[string]uuid.UUID) result, err := svc.repo.Locations.Create(ctx, GID, repo.LocationCreate{
{ Name: row.Location,
locations, err := svc.repo.Locations.Tree(ctx, GID, repo.TreeQuery{WithItems: false}) Description: "",
if err != nil {
return 0, err
}
// Traverse the tree and build a map of location full paths to IDs
// where the full path is the location name joined by slashes.
var traverse func(location *repo.TreeItem, path []string)
traverse = func(location *repo.TreeItem, path []string) {
path = append(path, location.Name)
locationMap[serializeLocation(path)] = location.ID
for _, child := range location.Children {
traverse(child, path)
}
}
for _, location := range locations {
traverse(&location, []string{})
}
}
// ========================================
// Import items
// Asset ID Pre-Check
highestAID := repo.AssetID(-1)
if svc.autoIncrementAssetID {
highestAID, err = svc.repo.Items.GetHighestAssetID(ctx, GID)
if err != nil {
return 0, err
}
}
finished := 0
for i := range sheet.Rows {
row := sheet.Rows[i]
createRequired := true
// ========================================
// Preflight check for existing item
if row.ImportRef != "" {
exists, err := svc.repo.Items.CheckRef(ctx, GID, row.ImportRef)
if err != nil {
return 0, fmt.Errorf("error checking for existing item with ref %q: %w", row.ImportRef, err)
}
if exists {
createRequired = false
}
}
// ========================================
// Pre-Create Labels as necessary
labelIds := make([]uuid.UUID, len(row.LabelStr))
for j := range row.LabelStr {
label := row.LabelStr[j]
id, ok := labelMap[label]
if !ok {
newLabel, err := svc.repo.Labels.Create(ctx, GID, repo.LabelCreate{Name: label})
if err != nil {
return 0, err
}
id = newLabel.ID
}
labelIds[j] = id
labelMap[label] = id
}
// ========================================
// Pre-Create Locations as necessary
path := serializeLocation(row.Location)
locationID, ok := locationMap[path]
if !ok { // Traverse the path of LocationStr and check each path element to see if it exists already, if not create it.
paths := []string{}
for i, pathElement := range row.Location {
paths = append(paths, pathElement)
path := serializeLocation(paths)
locationID, ok = locationMap[path]
if !ok {
parentID := uuid.Nil
// Get the parent ID
if i > 0 {
parentPath := serializeLocation(row.Location[:i])
parentID = locationMap[parentPath]
}
newLocation, err := svc.repo.Locations.Create(ctx, GID, repo.LocationCreate{
ParentID: parentID,
Name: pathElement,
}) })
if err != nil { if err != nil {
return 0, err return 0, err
} }
locationID = newLocation.ID locations[row.Location] = result.ID
} }
locationMap[path] = locationID // Labels
}
locationID, ok = locationMap[path] for _, label := range row.getLabels() {
if !ok { if _, exists := labels[label]; exists {
return 0, errors.New("failed to create location") continue
}
result, err := svc.repo.Labels.Create(ctx, GID, repo.LabelCreate{
Name: label,
Description: "",
})
if err != nil {
return 0, err
}
labels[label] = result.ID
} }
} }
var effAID repo.AssetID highest := repo.AssetID(-1)
if svc.autoIncrementAssetID && row.AssetID.Nil() { if svc.autoIncrementAssetID {
effAID = highestAID + 1 highest, err = svc.repo.Items.GetHighestAssetID(ctx, GID)
highestAID++ if err != nil {
} else { return 0, err
effAID = row.AssetID }
} }
// ======================================== // Create the items
// Create Item var count int
var item repo.ItemOut for _, row := range loaded {
switch { // Check Import Ref
case createRequired: if row.Item.ImportRef != "" {
newItem := repo.ItemCreate{ exists, err := svc.repo.Items.CheckRef(ctx, GID, row.Item.ImportRef)
ImportRef: row.ImportRef, if exists {
Name: row.Name, continue
Description: row.Description, }
AssetID: effAID, if err != nil {
log.Err(err).Msg("error checking import ref")
}
}
locationID := locations[row.Location]
labelIDs := []uuid.UUID{}
for _, label := range row.getLabels() {
labelIDs = append(labelIDs, labels[label])
}
log.Info().
Str("name", row.Item.Name).
Str("location", row.Location).
Msgf("Creating Item: %s", row.Item.Name)
data := repo.ItemCreate{
ImportRef: row.Item.ImportRef,
Name: row.Item.Name,
Description: row.Item.Description,
LabelIDs: labelIDs,
LocationID: locationID, LocationID: locationID,
LabelIDs: labelIds,
} }
item, err = svc.repo.Items.Create(ctx, GID, newItem) if svc.autoIncrementAssetID {
highest++
data.AssetID = highest
}
result, err := svc.repo.Items.Create(ctx, GID, data)
if err != nil { if err != nil {
return 0, err return count, err
}
default:
item, err = svc.repo.Items.GetByRef(ctx, GID, row.ImportRef)
if err != nil {
return 0, err
}
} }
if item.ID == uuid.Nil { // Update the item with the rest of the data
panic("item ID is nil on import - this should never happen") _, err = svc.repo.Items.UpdateByGroup(ctx, GID, repo.ItemUpdate{
} // Edges
fields := make([]repo.ItemField, len(row.Fields))
for i := range row.Fields {
fields[i] = repo.ItemField{
Name: row.Fields[i].Name,
Type: "text",
TextValue: row.Fields[i].Value,
}
}
updateItem := repo.ItemUpdate{
ID: item.ID,
LabelIDs: labelIds,
LocationID: locationID, LocationID: locationID,
LabelIDs: labelIDs,
AssetID: data.AssetID,
Name: row.Name, // General Fields
Description: row.Description, ID: result.ID,
AssetID: effAID, Name: result.Name,
Insured: row.Insured, Description: result.Description,
Quantity: row.Quantity, Insured: row.Item.Insured,
Archived: row.Archived, Notes: row.Item.Notes,
Quantity: row.Item.Quantity,
PurchasePrice: row.PurchasePrice, // Identifies the item as imported
PurchaseFrom: row.PurchaseFrom, SerialNumber: row.Item.SerialNumber,
PurchaseTime: row.PurchaseTime, ModelNumber: row.Item.ModelNumber,
Manufacturer: row.Item.Manufacturer,
Manufacturer: row.Manufacturer, // Purchase
ModelNumber: row.ModelNumber, PurchaseFrom: row.Item.PurchaseFrom,
SerialNumber: row.SerialNumber, PurchasePrice: row.Item.PurchasePrice,
PurchaseTime: row.Item.PurchaseTime,
LifetimeWarranty: row.LifetimeWarranty, // Warranty
WarrantyExpires: row.WarrantyExpires, LifetimeWarranty: row.Item.LifetimeWarranty,
WarrantyDetails: row.WarrantyDetails, WarrantyExpires: row.Item.WarrantyExpires,
WarrantyDetails: row.Item.WarrantyDetails,
SoldTo: row.SoldTo, SoldTo: row.Item.SoldTo,
SoldTime: row.SoldTime, SoldPrice: row.Item.SoldPrice,
SoldPrice: row.SoldPrice, SoldTime: row.Item.SoldTime,
SoldNotes: row.SoldNotes, SoldNotes: row.Item.SoldNotes,
})
Notes: row.Notes,
Fields: fields,
}
item, err = svc.repo.Items.UpdateByGroup(ctx, GID, updateItem)
if err != nil { if err != nil {
return 0, err return count, err
} }
finished++ count++
} }
return count, nil
return finished, nil
}
func (svc *ItemService) ExportTSV(ctx context.Context, GID uuid.UUID) ([][]string, error) {
items, err := svc.repo.Items.GetAll(ctx, GID)
if err != nil {
return nil, err
}
sheet := reporting.IOSheet{}
err = sheet.ReadItems(ctx, items, GID, svc.repo)
if err != nil {
return nil, err
}
return sheet.TSV()
}
func (svc *ItemService) ExportBillOfMaterialsTSV(ctx context.Context, GID uuid.UUID) ([]byte, error) {
items, err := svc.repo.Items.GetAll(ctx, GID)
if err != nil {
return nil, err
}
return reporting.BillOfMaterialsTSV(items)
} }

View file

@ -23,7 +23,7 @@ func (svc *ItemService) AttachmentPath(ctx context.Context, attachmentId uuid.UU
func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) { func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
// Update Attachment // Update Attachment
attachment, err := svc.repo.Attachments.Update(ctx, data.ID, data) attachment, err := svc.repo.Attachments.Update(ctx, data.ID, attachment.Type(data.Type))
if err != nil { if err != nil {
return repo.ItemOut{}, err return repo.ItemOut{}, err
} }

View file

@ -0,0 +1,151 @@
package services
import (
"bytes"
"encoding/csv"
"errors"
"io"
"strconv"
"strings"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
)
func determineSeparator(data []byte) (rune, error) {
// First row
firstRow := bytes.Split(data, []byte("\n"))[0]
// find first comma or /t
comma := bytes.IndexByte(firstRow, ',')
tab := bytes.IndexByte(firstRow, '\t')
switch {
case comma == -1 && tab == -1:
return 0, errors.New("could not determine separator")
case tab > comma:
return '\t', nil
default:
return ',', nil
}
}
func ReadCsv(r io.Reader) ([][]string, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
reader := csv.NewReader(bytes.NewReader(data))
// Determine separator
sep, err := determineSeparator(data)
if err != nil {
return nil, err
}
reader.Comma = sep
return reader.ReadAll()
}
var ErrInvalidCsv = errors.New("invalid csv")
const NumOfCols = 21
func parseFloat(s string) float64 {
if s == "" {
return 0
}
f, _ := strconv.ParseFloat(s, 64)
return f
}
func parseBool(s string) bool {
switch strings.ToLower(s) {
case "true", "yes", "1":
return true
default:
return false
}
}
func parseInt(s string) int {
i, _ := strconv.Atoi(s)
return i
}
type csvRow struct {
Item repo.ItemOut
Location string
LabelStr string
}
func newCsvRow(row []string) csvRow {
return csvRow{
Location: row[1],
LabelStr: row[2],
Item: repo.ItemOut{
ItemSummary: repo.ItemSummary{
ImportRef: row[0],
Quantity: parseInt(row[3]),
Name: row[4],
Description: row[5],
Insured: parseBool(row[6]),
PurchasePrice: parseFloat(row[12]),
},
SerialNumber: row[7],
ModelNumber: row[8],
Manufacturer: row[9],
Notes: row[10],
PurchaseFrom: row[11],
PurchaseTime: types.DateFromString(row[13]),
LifetimeWarranty: parseBool(row[14]),
WarrantyExpires: types.DateFromString(row[15]),
WarrantyDetails: row[16],
SoldTo: row[17],
SoldPrice: parseFloat(row[18]),
SoldTime: types.DateFromString(row[19]),
SoldNotes: row[20],
},
}
}
func (c csvRow) getLabels() []string {
split := strings.Split(c.LabelStr, ";")
// Trim each
for i, s := range split {
split[i] = strings.TrimSpace(s)
}
// Remove empty
for i, s := range split {
if s == "" {
split = append(split[:i], split[i+1:]...)
}
}
return split
}
func (c csvRow) validate() []error {
var errs []error
add := func(err error) {
errs = append(errs, err)
}
required := func(s string, name string) {
if s == "" {
add(errors.New(name + " is required"))
}
}
required(c.Location, "Location")
required(c.Item.Name, "Name")
return errs
}

View file

@ -0,0 +1,164 @@
package services
import (
"bytes"
_ "embed"
"encoding/csv"
"fmt"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
//go:embed .testdata/import.csv
var CSVData_Comma []byte
//go:embed .testdata/import.tsv
var CSVData_Tab []byte
func loadcsv() [][]string {
reader := csv.NewReader(bytes.NewReader(CSVData_Comma))
records, err := reader.ReadAll()
if err != nil {
panic(err)
}
return records
}
func Test_CorrectDateParsing(t *testing.T) {
t.Parallel()
expected := []time.Time{
time.Date(2021, 10, 13, 0, 0, 0, 0, time.UTC),
time.Date(2021, 10, 15, 0, 0, 0, 0, time.UTC),
time.Date(2021, 10, 13, 0, 0, 0, 0, time.UTC),
time.Date(2020, 10, 21, 0, 0, 0, 0, time.UTC),
time.Date(2020, 10, 14, 0, 0, 0, 0, time.UTC),
time.Date(2020, 9, 30, 0, 0, 0, 0, time.UTC),
}
records := loadcsv()
for i, record := range records {
if i == 0 {
continue
}
entity := newCsvRow(record)
expected := expected[i-1]
assert.Equal(t, expected, entity.Item.PurchaseTime.Time(), fmt.Sprintf("Failed on row %d", i))
assert.Equal(t, expected, entity.Item.WarrantyExpires.Time(), fmt.Sprintf("Failed on row %d", i))
assert.Equal(t, expected, entity.Item.SoldTime.Time(), fmt.Sprintf("Failed on row %d", i))
}
}
func Test_csvRow_getLabels(t *testing.T) {
type fields struct {
LabelStr string
}
tests := []struct {
name string
fields fields
want []string
}{
{
name: "basic test",
fields: fields{
LabelStr: "IOT;Home Assistant;Z-Wave",
},
want: []string{"IOT", "Home Assistant", "Z-Wave"},
},
{
name: "no labels",
fields: fields{
LabelStr: "",
},
want: []string{},
},
{
name: "single label",
fields: fields{
LabelStr: "IOT",
},
want: []string{"IOT"},
},
{
name: "trailing semicolon",
fields: fields{
LabelStr: "IOT;",
},
want: []string{"IOT"},
},
{
name: "whitespace",
fields: fields{
LabelStr: " IOT; Home Assistant; Z-Wave ",
},
want: []string{"IOT", "Home Assistant", "Z-Wave"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := csvRow{
LabelStr: tt.fields.LabelStr,
}
if got := c.getLabels(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("csvRow.getLabels() = %v, want %v", got, tt.want)
}
})
}
}
func Test_determineSeparator(t *testing.T) {
type args struct {
data []byte
}
tests := []struct {
name string
args args
want rune
wantErr bool
}{
{
name: "comma",
args: args{
data: CSVData_Comma,
},
want: ',',
wantErr: false,
},
{
name: "tab",
args: args{
data: CSVData_Tab,
},
want: '\t',
wantErr: false,
},
{
name: "invalid",
args: args{
data: []byte("a;b;c"),
},
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := determineSeparator(tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("determineSeparator() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("determineSeparator() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -0,0 +1,78 @@
package services
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/stretchr/testify/assert"
)
func TestItemService_CsvImport(t *testing.T) {
data := loadcsv()
svc := &ItemService{
repo: tRepos,
}
count, err := svc.CsvImport(context.Background(), tGroup.ID, data)
assert.Equal(t, 6, count)
assert.NoError(t, err)
// Check import refs are deduplicated
count, err = svc.CsvImport(context.Background(), tGroup.ID, data)
assert.Equal(t, 0, count)
assert.NoError(t, err)
items, err := svc.repo.Items.GetAll(context.Background(), tGroup.ID)
assert.NoError(t, err)
t.Cleanup(func() {
for _, item := range items {
err := svc.repo.Items.Delete(context.Background(), item.ID)
assert.NoError(t, err)
}
})
assert.Equal(t, len(items), 6)
dataCsv := []csvRow{}
for _, item := range data {
dataCsv = append(dataCsv, newCsvRow(item))
}
allLocation, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID, repo.LocationQuery{})
assert.NoError(t, err)
locNames := []string{}
for _, loc := range allLocation {
locNames = append(locNames, loc.Name)
}
allLabels, err := tRepos.Labels.GetAll(context.Background(), tGroup.ID)
assert.NoError(t, err)
labelNames := []string{}
for _, label := range allLabels {
labelNames = append(labelNames, label.Name)
}
ids := []uuid.UUID{}
t.Cleanup((func() {
for _, id := range ids {
err := svc.repo.Items.Delete(context.Background(), id)
assert.NoError(t, err)
}
}))
for _, item := range items {
assert.Contains(t, locNames, item.Location.Name)
for _, label := range item.Labels {
assert.Contains(t, labelNames, label.Name)
}
for _, csvRow := range dataCsv {
if csvRow.Item.Name == item.Name {
assert.Equal(t, csvRow.Item.Description, item.Description)
assert.Equal(t, csvRow.Item.Quantity, item.Quantity)
assert.Equal(t, csvRow.Item.Insured, item.Insured)
}
}
}
}

View file

@ -61,7 +61,6 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
switch data.GroupToken { switch data.GroupToken {
case "": case "":
log.Debug().Msg("creating new group")
creatingGroup = true creatingGroup = true
group, err = svc.repos.Groups.GroupCreate(ctx, "Home") group, err = svc.repos.Groups.GroupCreate(ctx, "Home")
if err != nil { if err != nil {
@ -69,7 +68,6 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
return repo.UserOut{}, err return repo.UserOut{}, err
} }
default: default:
log.Debug().Msg("joining existing group")
token, err = svc.repos.Groups.InvitationGet(ctx, hasher.HashToken(data.GroupToken)) token, err = svc.repos.Groups.InvitationGet(ctx, hasher.HashToken(data.GroupToken))
if err != nil { if err != nil {
log.Err(err).Msg("Failed to get invitation token") log.Err(err).Msg("Failed to get invitation token")
@ -96,14 +94,14 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration)
// Create the default labels and locations for the group. // Create the default labels and locations for the group.
if creatingGroup { if creatingGroup {
for _, label := range defaultLabels() { for _, label := range defaultLabels() {
_, err := svc.repos.Labels.Create(ctx, usr.GroupID, label) _, err := svc.repos.Labels.Create(ctx, group.ID, label)
if err != nil { if err != nil {
return repo.UserOut{}, err return repo.UserOut{}, err
} }
} }
for _, location := range defaultLocations() { for _, location := range defaultLocations() {
_, err := svc.repos.Locations.Create(ctx, usr.GroupID, location) _, err := svc.repos.Locations.Create(ctx, group.ID, location)
if err != nil { if err != nil {
return repo.UserOut{}, err return repo.UserOut{}, err
} }
@ -140,18 +138,12 @@ func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data repo.
// ============================================================================ // ============================================================================
// User Authentication // User Authentication
func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID, extendedSession bool) (UserAuthTokenDetail, error) { func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID) (UserAuthTokenDetail, error) {
attachmentToken := hasher.GenerateToken() attachmentToken := hasher.GenerateToken()
expiresAt := time.Now().Add(oneWeek)
if extendedSession {
expiresAt = time.Now().Add(oneWeek * 4)
}
attachmentData := repo.UserAuthTokenCreate{ attachmentData := repo.UserAuthTokenCreate{
UserID: userId, UserID: userId,
TokenHash: attachmentToken.Hash, TokenHash: attachmentToken.Hash,
ExpiresAt: expiresAt, ExpiresAt: time.Now().Add(oneWeek),
} }
_, err := svc.repos.AuthTokens.CreateToken(ctx, attachmentData, authroles.RoleAttachments) _, err := svc.repos.AuthTokens.CreateToken(ctx, attachmentData, authroles.RoleAttachments)
@ -163,7 +155,7 @@ func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID
data := repo.UserAuthTokenCreate{ data := repo.UserAuthTokenCreate{
UserID: userId, UserID: userId,
TokenHash: userToken.Hash, TokenHash: userToken.Hash,
ExpiresAt: expiresAt, ExpiresAt: time.Now().Add(oneWeek),
} }
created, err := svc.repos.AuthTokens.CreateToken(ctx, data, authroles.RoleUser) created, err := svc.repos.AuthTokens.CreateToken(ctx, data, authroles.RoleUser)
@ -178,7 +170,7 @@ func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID
}, nil }, nil
} }
func (svc *UserService) Login(ctx context.Context, username, password string, extendedSession bool) (UserAuthTokenDetail, error) { func (svc *UserService) Login(ctx context.Context, username, password string) (UserAuthTokenDetail, error) {
usr, err := svc.repos.Users.GetOneEmail(ctx, username) usr, err := svc.repos.Users.GetOneEmail(ctx, username)
if err != nil { if err != nil {
// SECURITY: Perform hash to ensure response times are the same // SECURITY: Perform hash to ensure response times are the same
@ -190,7 +182,7 @@ func (svc *UserService) Login(ctx context.Context, username, password string, ex
return UserAuthTokenDetail{}, ErrorInvalidLogin return UserAuthTokenDetail{}, ErrorInvalidLogin
} }
return svc.createSessionToken(ctx, usr.ID, extendedSession) return svc.createSessionToken(ctx, usr.ID)
} }
func (svc *UserService) Logout(ctx context.Context, token string) error { func (svc *UserService) Logout(ctx context.Context, token string) error {
@ -207,7 +199,7 @@ func (svc *UserService) RenewToken(ctx context.Context, token string) (UserAuthT
return UserAuthTokenDetail{}, ErrorInvalidToken return UserAuthTokenDetail{}, ErrorInvalidToken
} }
return svc.createSessionToken(ctx, dbToken.ID, false) return svc.createSessionToken(ctx, dbToken.ID)
} }
// DeleteSelf deletes the user that is currently logged based of the provided UUID // DeleteSelf deletes the user that is currently logged based of the provided UUID

View file

@ -7,7 +7,6 @@ import (
"strings" "strings"
"time" "time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment" "github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
@ -26,14 +25,11 @@ type Attachment struct {
UpdatedAt time.Time `json:"updated_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"`
// Type holds the value of the "type" field. // Type holds the value of the "type" field.
Type attachment.Type `json:"type,omitempty"` Type attachment.Type `json:"type,omitempty"`
// Primary holds the value of the "primary" field.
Primary bool `json:"primary,omitempty"`
// Edges holds the relations/edges for other nodes in the graph. // Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the AttachmentQuery when eager-loading is set. // The values are being populated by the AttachmentQuery when eager-loading is set.
Edges AttachmentEdges `json:"edges"` Edges AttachmentEdges `json:"edges"`
document_attachments *uuid.UUID document_attachments *uuid.UUID
item_attachments *uuid.UUID item_attachments *uuid.UUID
selectValues sql.SelectValues
} }
// AttachmentEdges holds the relations/edges for other nodes in the graph. // AttachmentEdges holds the relations/edges for other nodes in the graph.
@ -78,8 +74,6 @@ func (*Attachment) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns)) values := make([]any, len(columns))
for i := range columns { for i := range columns {
switch columns[i] { switch columns[i] {
case attachment.FieldPrimary:
values[i] = new(sql.NullBool)
case attachment.FieldType: case attachment.FieldType:
values[i] = new(sql.NullString) values[i] = new(sql.NullString)
case attachment.FieldCreatedAt, attachment.FieldUpdatedAt: case attachment.FieldCreatedAt, attachment.FieldUpdatedAt:
@ -91,7 +85,7 @@ func (*Attachment) scanValues(columns []string) ([]any, error) {
case attachment.ForeignKeys[1]: // item_attachments case attachment.ForeignKeys[1]: // item_attachments
values[i] = &sql.NullScanner{S: new(uuid.UUID)} values[i] = &sql.NullScanner{S: new(uuid.UUID)}
default: default:
values[i] = new(sql.UnknownType) return nil, fmt.Errorf("unexpected column %q for type Attachment", columns[i])
} }
} }
return values, nil return values, nil
@ -129,12 +123,6 @@ func (a *Attachment) assignValues(columns []string, values []any) error {
} else if value.Valid { } else if value.Valid {
a.Type = attachment.Type(value.String) a.Type = attachment.Type(value.String)
} }
case attachment.FieldPrimary:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field primary", values[i])
} else if value.Valid {
a.Primary = value.Bool
}
case attachment.ForeignKeys[0]: case attachment.ForeignKeys[0]:
if value, ok := values[i].(*sql.NullScanner); !ok { if value, ok := values[i].(*sql.NullScanner); !ok {
return fmt.Errorf("unexpected type %T for field document_attachments", values[i]) return fmt.Errorf("unexpected type %T for field document_attachments", values[i])
@ -149,19 +137,11 @@ func (a *Attachment) assignValues(columns []string, values []any) error {
a.item_attachments = new(uuid.UUID) a.item_attachments = new(uuid.UUID)
*a.item_attachments = *value.S.(*uuid.UUID) *a.item_attachments = *value.S.(*uuid.UUID)
} }
default:
a.selectValues.Set(columns[i], values[i])
} }
} }
return nil return nil
} }
// Value returns the ent.Value that was dynamically selected and assigned to the Attachment.
// This includes values selected through modifiers, order, etc.
func (a *Attachment) Value(name string) (ent.Value, error) {
return a.selectValues.Get(name)
}
// QueryItem queries the "item" edge of the Attachment entity. // QueryItem queries the "item" edge of the Attachment entity.
func (a *Attachment) QueryItem() *ItemQuery { func (a *Attachment) QueryItem() *ItemQuery {
return NewAttachmentClient(a.config).QueryItem(a) return NewAttachmentClient(a.config).QueryItem(a)
@ -203,9 +183,6 @@ func (a *Attachment) String() string {
builder.WriteString(", ") builder.WriteString(", ")
builder.WriteString("type=") builder.WriteString("type=")
builder.WriteString(fmt.Sprintf("%v", a.Type)) builder.WriteString(fmt.Sprintf("%v", a.Type))
builder.WriteString(", ")
builder.WriteString("primary=")
builder.WriteString(fmt.Sprintf("%v", a.Primary))
builder.WriteByte(')') builder.WriteByte(')')
return builder.String() return builder.String()
} }

View file

@ -6,8 +6,6 @@ import (
"fmt" "fmt"
"time" "time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -22,8 +20,6 @@ const (
FieldUpdatedAt = "updated_at" FieldUpdatedAt = "updated_at"
// FieldType holds the string denoting the type field in the database. // FieldType holds the string denoting the type field in the database.
FieldType = "type" FieldType = "type"
// FieldPrimary holds the string denoting the primary field in the database.
FieldPrimary = "primary"
// EdgeItem holds the string denoting the item edge name in mutations. // EdgeItem holds the string denoting the item edge name in mutations.
EdgeItem = "item" EdgeItem = "item"
// EdgeDocument holds the string denoting the document edge name in mutations. // EdgeDocument holds the string denoting the document edge name in mutations.
@ -52,7 +48,6 @@ var Columns = []string{
FieldCreatedAt, FieldCreatedAt,
FieldUpdatedAt, FieldUpdatedAt,
FieldType, FieldType,
FieldPrimary,
} }
// ForeignKeys holds the SQL foreign-keys that are owned by the "attachments" // ForeignKeys holds the SQL foreign-keys that are owned by the "attachments"
@ -84,8 +79,6 @@ var (
DefaultUpdatedAt func() time.Time DefaultUpdatedAt func() time.Time
// UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
UpdateDefaultUpdatedAt func() time.Time UpdateDefaultUpdatedAt func() time.Time
// DefaultPrimary holds the default value on creation for the "primary" field.
DefaultPrimary bool
// DefaultID holds the default value on creation for the "id" field. // DefaultID holds the default value on creation for the "id" field.
DefaultID func() uuid.UUID DefaultID func() uuid.UUID
) )
@ -118,59 +111,3 @@ func TypeValidator(_type Type) error {
return fmt.Errorf("attachment: invalid enum value for type field: %q", _type) return fmt.Errorf("attachment: invalid enum value for type field: %q", _type)
} }
} }
// OrderOption defines the ordering options for the Attachment queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByCreatedAt orders the results by the created_at field.
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
}
// ByUpdatedAt orders the results by the updated_at field.
func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
}
// ByType orders the results by the type field.
func ByType(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldType, opts...).ToFunc()
}
// ByPrimary orders the results by the primary field.
func ByPrimary(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldPrimary, opts...).ToFunc()
}
// ByItemField orders the results by item field.
func ByItemField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newItemStep(), sql.OrderByField(field, opts...))
}
}
// ByDocumentField orders the results by document field.
func ByDocumentField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newDocumentStep(), sql.OrderByField(field, opts...))
}
}
func newItemStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(ItemInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, ItemTable, ItemColumn),
)
}
func newDocumentStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(DocumentInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, DocumentTable, DocumentColumn),
)
}

View file

@ -66,11 +66,6 @@ func UpdatedAt(v time.Time) predicate.Attachment {
return predicate.Attachment(sql.FieldEQ(FieldUpdatedAt, v)) return predicate.Attachment(sql.FieldEQ(FieldUpdatedAt, v))
} }
// Primary applies equality check predicate on the "primary" field. It's identical to PrimaryEQ.
func Primary(v bool) predicate.Attachment {
return predicate.Attachment(sql.FieldEQ(FieldPrimary, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field. // CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.Attachment { func CreatedAtEQ(v time.Time) predicate.Attachment {
return predicate.Attachment(sql.FieldEQ(FieldCreatedAt, v)) return predicate.Attachment(sql.FieldEQ(FieldCreatedAt, v))
@ -171,16 +166,6 @@ func TypeNotIn(vs ...Type) predicate.Attachment {
return predicate.Attachment(sql.FieldNotIn(FieldType, vs...)) return predicate.Attachment(sql.FieldNotIn(FieldType, vs...))
} }
// PrimaryEQ applies the EQ predicate on the "primary" field.
func PrimaryEQ(v bool) predicate.Attachment {
return predicate.Attachment(sql.FieldEQ(FieldPrimary, v))
}
// PrimaryNEQ applies the NEQ predicate on the "primary" field.
func PrimaryNEQ(v bool) predicate.Attachment {
return predicate.Attachment(sql.FieldNEQ(FieldPrimary, v))
}
// HasItem applies the HasEdge predicate on the "item" edge. // HasItem applies the HasEdge predicate on the "item" edge.
func HasItem() predicate.Attachment { func HasItem() predicate.Attachment {
return predicate.Attachment(func(s *sql.Selector) { return predicate.Attachment(func(s *sql.Selector) {
@ -195,7 +180,11 @@ func HasItem() predicate.Attachment {
// HasItemWith applies the HasEdge predicate on the "item" edge with a given conditions (other predicates). // HasItemWith applies the HasEdge predicate on the "item" edge with a given conditions (other predicates).
func HasItemWith(preds ...predicate.Item) predicate.Attachment { func HasItemWith(preds ...predicate.Item) predicate.Attachment {
return predicate.Attachment(func(s *sql.Selector) { return predicate.Attachment(func(s *sql.Selector) {
step := newItemStep() step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(ItemInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, ItemTable, ItemColumn),
)
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds { for _, p := range preds {
p(s) p(s)
@ -218,7 +207,11 @@ func HasDocument() predicate.Attachment {
// HasDocumentWith applies the HasEdge predicate on the "document" edge with a given conditions (other predicates). // HasDocumentWith applies the HasEdge predicate on the "document" edge with a given conditions (other predicates).
func HasDocumentWith(preds ...predicate.Document) predicate.Attachment { func HasDocumentWith(preds ...predicate.Document) predicate.Attachment {
return predicate.Attachment(func(s *sql.Selector) { return predicate.Attachment(func(s *sql.Selector) {
step := newDocumentStep() step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(DocumentInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, DocumentTable, DocumentColumn),
)
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds { for _, p := range preds {
p(s) p(s)

View file

@ -65,20 +65,6 @@ func (ac *AttachmentCreate) SetNillableType(a *attachment.Type) *AttachmentCreat
return ac return ac
} }
// SetPrimary sets the "primary" field.
func (ac *AttachmentCreate) SetPrimary(b bool) *AttachmentCreate {
ac.mutation.SetPrimary(b)
return ac
}
// SetNillablePrimary sets the "primary" field if the given value is not nil.
func (ac *AttachmentCreate) SetNillablePrimary(b *bool) *AttachmentCreate {
if b != nil {
ac.SetPrimary(*b)
}
return ac
}
// SetID sets the "id" field. // SetID sets the "id" field.
func (ac *AttachmentCreate) SetID(u uuid.UUID) *AttachmentCreate { func (ac *AttachmentCreate) SetID(u uuid.UUID) *AttachmentCreate {
ac.mutation.SetID(u) ac.mutation.SetID(u)
@ -123,7 +109,7 @@ func (ac *AttachmentCreate) Mutation() *AttachmentMutation {
// Save creates the Attachment in the database. // Save creates the Attachment in the database.
func (ac *AttachmentCreate) Save(ctx context.Context) (*Attachment, error) { func (ac *AttachmentCreate) Save(ctx context.Context) (*Attachment, error) {
ac.defaults() ac.defaults()
return withHooks(ctx, ac.sqlSave, ac.mutation, ac.hooks) return withHooks[*Attachment, AttachmentMutation](ctx, ac.sqlSave, ac.mutation, ac.hooks)
} }
// SaveX calls Save and panics if Save returns an error. // SaveX calls Save and panics if Save returns an error.
@ -162,10 +148,6 @@ func (ac *AttachmentCreate) defaults() {
v := attachment.DefaultType v := attachment.DefaultType
ac.mutation.SetType(v) ac.mutation.SetType(v)
} }
if _, ok := ac.mutation.Primary(); !ok {
v := attachment.DefaultPrimary
ac.mutation.SetPrimary(v)
}
if _, ok := ac.mutation.ID(); !ok { if _, ok := ac.mutation.ID(); !ok {
v := attachment.DefaultID() v := attachment.DefaultID()
ac.mutation.SetID(v) ac.mutation.SetID(v)
@ -188,9 +170,6 @@ func (ac *AttachmentCreate) check() error {
return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Attachment.type": %w`, err)} return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Attachment.type": %w`, err)}
} }
} }
if _, ok := ac.mutation.Primary(); !ok {
return &ValidationError{Name: "primary", err: errors.New(`ent: missing required field "Attachment.primary"`)}
}
if _, ok := ac.mutation.ItemID(); !ok { if _, ok := ac.mutation.ItemID(); !ok {
return &ValidationError{Name: "item", err: errors.New(`ent: missing required edge "Attachment.item"`)} return &ValidationError{Name: "item", err: errors.New(`ent: missing required edge "Attachment.item"`)}
} }
@ -244,10 +223,6 @@ func (ac *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
_spec.SetField(attachment.FieldType, field.TypeEnum, value) _spec.SetField(attachment.FieldType, field.TypeEnum, value)
_node.Type = value _node.Type = value
} }
if value, ok := ac.mutation.Primary(); ok {
_spec.SetField(attachment.FieldPrimary, field.TypeBool, value)
_node.Primary = value
}
if nodes := ac.mutation.ItemIDs(); len(nodes) > 0 { if nodes := ac.mutation.ItemIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2O,
@ -256,7 +231,10 @@ func (ac *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
Columns: []string{attachment.ItemColumn}, Columns: []string{attachment.ItemColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(item.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: item.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -273,7 +251,10 @@ func (ac *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
Columns: []string{attachment.DocumentColumn}, Columns: []string{attachment.DocumentColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(document.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: document.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -309,8 +290,8 @@ func (acb *AttachmentCreateBulk) Save(ctx context.Context) ([]*Attachment, error
return nil, err return nil, err
} }
builder.mutation = mutation builder.mutation = mutation
var err error
nodes[i], specs[i] = builder.createSpec() nodes[i], specs[i] = builder.createSpec()
var err error
if i < len(mutators)-1 { if i < len(mutators)-1 {
_, err = mutators[i+1].Mutate(root, acb.builders[i+1].mutation) _, err = mutators[i+1].Mutate(root, acb.builders[i+1].mutation)
} else { } else {

View file

@ -27,7 +27,7 @@ func (ad *AttachmentDelete) Where(ps ...predicate.Attachment) *AttachmentDelete
// Exec executes the deletion query and returns how many vertices were deleted. // Exec executes the deletion query and returns how many vertices were deleted.
func (ad *AttachmentDelete) Exec(ctx context.Context) (int, error) { func (ad *AttachmentDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, ad.sqlExec, ad.mutation, ad.hooks) return withHooks[int, AttachmentMutation](ctx, ad.sqlExec, ad.mutation, ad.hooks)
} }
// ExecX is like Exec, but panics if an error occurs. // ExecX is like Exec, but panics if an error occurs.

View file

@ -21,7 +21,7 @@ import (
type AttachmentQuery struct { type AttachmentQuery struct {
config config
ctx *QueryContext ctx *QueryContext
order []attachment.OrderOption order []OrderFunc
inters []Interceptor inters []Interceptor
predicates []predicate.Attachment predicates []predicate.Attachment
withItem *ItemQuery withItem *ItemQuery
@ -58,7 +58,7 @@ func (aq *AttachmentQuery) Unique(unique bool) *AttachmentQuery {
} }
// Order specifies how the records should be ordered. // Order specifies how the records should be ordered.
func (aq *AttachmentQuery) Order(o ...attachment.OrderOption) *AttachmentQuery { func (aq *AttachmentQuery) Order(o ...OrderFunc) *AttachmentQuery {
aq.order = append(aq.order, o...) aq.order = append(aq.order, o...)
return aq return aq
} }
@ -296,7 +296,7 @@ func (aq *AttachmentQuery) Clone() *AttachmentQuery {
return &AttachmentQuery{ return &AttachmentQuery{
config: aq.config, config: aq.config,
ctx: aq.ctx.Clone(), ctx: aq.ctx.Clone(),
order: append([]attachment.OrderOption{}, aq.order...), order: append([]OrderFunc{}, aq.order...),
inters: append([]Interceptor{}, aq.inters...), inters: append([]Interceptor{}, aq.inters...),
predicates: append([]predicate.Attachment{}, aq.predicates...), predicates: append([]predicate.Attachment{}, aq.predicates...),
withItem: aq.withItem.Clone(), withItem: aq.withItem.Clone(),

View file

@ -51,20 +51,6 @@ func (au *AttachmentUpdate) SetNillableType(a *attachment.Type) *AttachmentUpdat
return au return au
} }
// SetPrimary sets the "primary" field.
func (au *AttachmentUpdate) SetPrimary(b bool) *AttachmentUpdate {
au.mutation.SetPrimary(b)
return au
}
// SetNillablePrimary sets the "primary" field if the given value is not nil.
func (au *AttachmentUpdate) SetNillablePrimary(b *bool) *AttachmentUpdate {
if b != nil {
au.SetPrimary(*b)
}
return au
}
// SetItemID sets the "item" edge to the Item entity by ID. // SetItemID sets the "item" edge to the Item entity by ID.
func (au *AttachmentUpdate) SetItemID(id uuid.UUID) *AttachmentUpdate { func (au *AttachmentUpdate) SetItemID(id uuid.UUID) *AttachmentUpdate {
au.mutation.SetItemID(id) au.mutation.SetItemID(id)
@ -107,7 +93,7 @@ func (au *AttachmentUpdate) ClearDocument() *AttachmentUpdate {
// Save executes the query and returns the number of nodes affected by the update operation. // Save executes the query and returns the number of nodes affected by the update operation.
func (au *AttachmentUpdate) Save(ctx context.Context) (int, error) { func (au *AttachmentUpdate) Save(ctx context.Context) (int, error) {
au.defaults() au.defaults()
return withHooks(ctx, au.sqlSave, au.mutation, au.hooks) return withHooks[int, AttachmentMutation](ctx, au.sqlSave, au.mutation, au.hooks)
} }
// SaveX is like Save, but panics if an error occurs. // SaveX is like Save, but panics if an error occurs.
@ -174,9 +160,6 @@ func (au *AttachmentUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := au.mutation.GetType(); ok { if value, ok := au.mutation.GetType(); ok {
_spec.SetField(attachment.FieldType, field.TypeEnum, value) _spec.SetField(attachment.FieldType, field.TypeEnum, value)
} }
if value, ok := au.mutation.Primary(); ok {
_spec.SetField(attachment.FieldPrimary, field.TypeBool, value)
}
if au.mutation.ItemCleared() { if au.mutation.ItemCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2O,
@ -185,7 +168,10 @@ func (au *AttachmentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{attachment.ItemColumn}, Columns: []string{attachment.ItemColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(item.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: item.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -198,7 +184,10 @@ func (au *AttachmentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{attachment.ItemColumn}, Columns: []string{attachment.ItemColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(item.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: item.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -214,7 +203,10 @@ func (au *AttachmentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{attachment.DocumentColumn}, Columns: []string{attachment.DocumentColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(document.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: document.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -227,7 +219,10 @@ func (au *AttachmentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{attachment.DocumentColumn}, Columns: []string{attachment.DocumentColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(document.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: document.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -275,20 +270,6 @@ func (auo *AttachmentUpdateOne) SetNillableType(a *attachment.Type) *AttachmentU
return auo return auo
} }
// SetPrimary sets the "primary" field.
func (auo *AttachmentUpdateOne) SetPrimary(b bool) *AttachmentUpdateOne {
auo.mutation.SetPrimary(b)
return auo
}
// SetNillablePrimary sets the "primary" field if the given value is not nil.
func (auo *AttachmentUpdateOne) SetNillablePrimary(b *bool) *AttachmentUpdateOne {
if b != nil {
auo.SetPrimary(*b)
}
return auo
}
// SetItemID sets the "item" edge to the Item entity by ID. // SetItemID sets the "item" edge to the Item entity by ID.
func (auo *AttachmentUpdateOne) SetItemID(id uuid.UUID) *AttachmentUpdateOne { func (auo *AttachmentUpdateOne) SetItemID(id uuid.UUID) *AttachmentUpdateOne {
auo.mutation.SetItemID(id) auo.mutation.SetItemID(id)
@ -344,7 +325,7 @@ func (auo *AttachmentUpdateOne) Select(field string, fields ...string) *Attachme
// Save executes the query and returns the updated Attachment entity. // Save executes the query and returns the updated Attachment entity.
func (auo *AttachmentUpdateOne) Save(ctx context.Context) (*Attachment, error) { func (auo *AttachmentUpdateOne) Save(ctx context.Context) (*Attachment, error) {
auo.defaults() auo.defaults()
return withHooks(ctx, auo.sqlSave, auo.mutation, auo.hooks) return withHooks[*Attachment, AttachmentMutation](ctx, auo.sqlSave, auo.mutation, auo.hooks)
} }
// SaveX is like Save, but panics if an error occurs. // SaveX is like Save, but panics if an error occurs.
@ -428,9 +409,6 @@ func (auo *AttachmentUpdateOne) sqlSave(ctx context.Context) (_node *Attachment,
if value, ok := auo.mutation.GetType(); ok { if value, ok := auo.mutation.GetType(); ok {
_spec.SetField(attachment.FieldType, field.TypeEnum, value) _spec.SetField(attachment.FieldType, field.TypeEnum, value)
} }
if value, ok := auo.mutation.Primary(); ok {
_spec.SetField(attachment.FieldPrimary, field.TypeBool, value)
}
if auo.mutation.ItemCleared() { if auo.mutation.ItemCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2O,
@ -439,7 +417,10 @@ func (auo *AttachmentUpdateOne) sqlSave(ctx context.Context) (_node *Attachment,
Columns: []string{attachment.ItemColumn}, Columns: []string{attachment.ItemColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(item.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: item.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -452,7 +433,10 @@ func (auo *AttachmentUpdateOne) sqlSave(ctx context.Context) (_node *Attachment,
Columns: []string{attachment.ItemColumn}, Columns: []string{attachment.ItemColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(item.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: item.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -468,7 +452,10 @@ func (auo *AttachmentUpdateOne) sqlSave(ctx context.Context) (_node *Attachment,
Columns: []string{attachment.DocumentColumn}, Columns: []string{attachment.DocumentColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(document.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: document.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -481,7 +468,10 @@ func (auo *AttachmentUpdateOne) sqlSave(ctx context.Context) (_node *Attachment,
Columns: []string{attachment.DocumentColumn}, Columns: []string{attachment.DocumentColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(document.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: document.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {

View file

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"entgo.io/ent"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent/authroles" "github.com/hay-kot/homebox/backend/internal/data/ent/authroles"
@ -24,7 +23,6 @@ type AuthRoles struct {
// The values are being populated by the AuthRolesQuery when eager-loading is set. // The values are being populated by the AuthRolesQuery when eager-loading is set.
Edges AuthRolesEdges `json:"edges"` Edges AuthRolesEdges `json:"edges"`
auth_tokens_roles *uuid.UUID auth_tokens_roles *uuid.UUID
selectValues sql.SelectValues
} }
// AuthRolesEdges holds the relations/edges for other nodes in the graph. // AuthRolesEdges holds the relations/edges for other nodes in the graph.
@ -61,7 +59,7 @@ func (*AuthRoles) scanValues(columns []string) ([]any, error) {
case authroles.ForeignKeys[0]: // auth_tokens_roles case authroles.ForeignKeys[0]: // auth_tokens_roles
values[i] = &sql.NullScanner{S: new(uuid.UUID)} values[i] = &sql.NullScanner{S: new(uuid.UUID)}
default: default:
values[i] = new(sql.UnknownType) return nil, fmt.Errorf("unexpected column %q for type AuthRoles", columns[i])
} }
} }
return values, nil return values, nil
@ -94,19 +92,11 @@ func (ar *AuthRoles) assignValues(columns []string, values []any) error {
ar.auth_tokens_roles = new(uuid.UUID) ar.auth_tokens_roles = new(uuid.UUID)
*ar.auth_tokens_roles = *value.S.(*uuid.UUID) *ar.auth_tokens_roles = *value.S.(*uuid.UUID)
} }
default:
ar.selectValues.Set(columns[i], values[i])
} }
} }
return nil return nil
} }
// Value returns the ent.Value that was dynamically selected and assigned to the AuthRoles.
// This includes values selected through modifiers, order, etc.
func (ar *AuthRoles) Value(name string) (ent.Value, error) {
return ar.selectValues.Get(name)
}
// QueryToken queries the "token" edge of the AuthRoles entity. // QueryToken queries the "token" edge of the AuthRoles entity.
func (ar *AuthRoles) QueryToken() *AuthTokensQuery { func (ar *AuthRoles) QueryToken() *AuthTokensQuery {
return NewAuthRolesClient(ar.config).QueryToken(ar) return NewAuthRolesClient(ar.config).QueryToken(ar)

View file

@ -4,9 +4,6 @@ package authroles
import ( import (
"fmt" "fmt"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
) )
const ( const (
@ -82,30 +79,3 @@ func RoleValidator(r Role) error {
return fmt.Errorf("authroles: invalid enum value for role field: %q", r) return fmt.Errorf("authroles: invalid enum value for role field: %q", r)
} }
} }
// OrderOption defines the ordering options for the AuthRoles queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByRole orders the results by the role field.
func ByRole(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldRole, opts...).ToFunc()
}
// ByTokenField orders the results by token field.
func ByTokenField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newTokenStep(), sql.OrderByField(field, opts...))
}
}
func newTokenStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(TokenInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2O, true, TokenTable, TokenColumn),
)
}

View file

@ -87,7 +87,11 @@ func HasToken() predicate.AuthRoles {
// HasTokenWith applies the HasEdge predicate on the "token" edge with a given conditions (other predicates). // HasTokenWith applies the HasEdge predicate on the "token" edge with a given conditions (other predicates).
func HasTokenWith(preds ...predicate.AuthTokens) predicate.AuthRoles { func HasTokenWith(preds ...predicate.AuthTokens) predicate.AuthRoles {
return predicate.AuthRoles(func(s *sql.Selector) { return predicate.AuthRoles(func(s *sql.Selector) {
step := newTokenStep() step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(TokenInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2O, true, TokenTable, TokenColumn),
)
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds { for _, p := range preds {
p(s) p(s)

View file

@ -62,7 +62,7 @@ func (arc *AuthRolesCreate) Mutation() *AuthRolesMutation {
// Save creates the AuthRoles in the database. // Save creates the AuthRoles in the database.
func (arc *AuthRolesCreate) Save(ctx context.Context) (*AuthRoles, error) { func (arc *AuthRolesCreate) Save(ctx context.Context) (*AuthRoles, error) {
arc.defaults() arc.defaults()
return withHooks(ctx, arc.sqlSave, arc.mutation, arc.hooks) return withHooks[*AuthRoles, AuthRolesMutation](ctx, arc.sqlSave, arc.mutation, arc.hooks)
} }
// SaveX calls Save and panics if Save returns an error. // SaveX calls Save and panics if Save returns an error.
@ -143,7 +143,10 @@ func (arc *AuthRolesCreate) createSpec() (*AuthRoles, *sqlgraph.CreateSpec) {
Columns: []string{authroles.TokenColumn}, Columns: []string{authroles.TokenColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authtokens.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: authtokens.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -179,8 +182,8 @@ func (arcb *AuthRolesCreateBulk) Save(ctx context.Context) ([]*AuthRoles, error)
return nil, err return nil, err
} }
builder.mutation = mutation builder.mutation = mutation
var err error
nodes[i], specs[i] = builder.createSpec() nodes[i], specs[i] = builder.createSpec()
var err error
if i < len(mutators)-1 { if i < len(mutators)-1 {
_, err = mutators[i+1].Mutate(root, arcb.builders[i+1].mutation) _, err = mutators[i+1].Mutate(root, arcb.builders[i+1].mutation)
} else { } else {

View file

@ -27,7 +27,7 @@ func (ard *AuthRolesDelete) Where(ps ...predicate.AuthRoles) *AuthRolesDelete {
// Exec executes the deletion query and returns how many vertices were deleted. // Exec executes the deletion query and returns how many vertices were deleted.
func (ard *AuthRolesDelete) Exec(ctx context.Context) (int, error) { func (ard *AuthRolesDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, ard.sqlExec, ard.mutation, ard.hooks) return withHooks[int, AuthRolesMutation](ctx, ard.sqlExec, ard.mutation, ard.hooks)
} }
// ExecX is like Exec, but panics if an error occurs. // ExecX is like Exec, but panics if an error occurs.

View file

@ -20,7 +20,7 @@ import (
type AuthRolesQuery struct { type AuthRolesQuery struct {
config config
ctx *QueryContext ctx *QueryContext
order []authroles.OrderOption order []OrderFunc
inters []Interceptor inters []Interceptor
predicates []predicate.AuthRoles predicates []predicate.AuthRoles
withToken *AuthTokensQuery withToken *AuthTokensQuery
@ -56,7 +56,7 @@ func (arq *AuthRolesQuery) Unique(unique bool) *AuthRolesQuery {
} }
// Order specifies how the records should be ordered. // Order specifies how the records should be ordered.
func (arq *AuthRolesQuery) Order(o ...authroles.OrderOption) *AuthRolesQuery { func (arq *AuthRolesQuery) Order(o ...OrderFunc) *AuthRolesQuery {
arq.order = append(arq.order, o...) arq.order = append(arq.order, o...)
return arq return arq
} }
@ -272,7 +272,7 @@ func (arq *AuthRolesQuery) Clone() *AuthRolesQuery {
return &AuthRolesQuery{ return &AuthRolesQuery{
config: arq.config, config: arq.config,
ctx: arq.ctx.Clone(), ctx: arq.ctx.Clone(),
order: append([]authroles.OrderOption{}, arq.order...), order: append([]OrderFunc{}, arq.order...),
inters: append([]Interceptor{}, arq.inters...), inters: append([]Interceptor{}, arq.inters...),
predicates: append([]predicate.AuthRoles{}, arq.predicates...), predicates: append([]predicate.AuthRoles{}, arq.predicates...),
withToken: arq.withToken.Clone(), withToken: arq.withToken.Clone(),

View file

@ -75,7 +75,7 @@ func (aru *AuthRolesUpdate) ClearToken() *AuthRolesUpdate {
// Save executes the query and returns the number of nodes affected by the update operation. // Save executes the query and returns the number of nodes affected by the update operation.
func (aru *AuthRolesUpdate) Save(ctx context.Context) (int, error) { func (aru *AuthRolesUpdate) Save(ctx context.Context) (int, error) {
return withHooks(ctx, aru.sqlSave, aru.mutation, aru.hooks) return withHooks[int, AuthRolesMutation](ctx, aru.sqlSave, aru.mutation, aru.hooks)
} }
// SaveX is like Save, but panics if an error occurs. // SaveX is like Save, but panics if an error occurs.
@ -133,7 +133,10 @@ func (aru *AuthRolesUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{authroles.TokenColumn}, Columns: []string{authroles.TokenColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authtokens.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: authtokens.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -146,7 +149,10 @@ func (aru *AuthRolesUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{authroles.TokenColumn}, Columns: []string{authroles.TokenColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authtokens.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: authtokens.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -233,7 +239,7 @@ func (aruo *AuthRolesUpdateOne) Select(field string, fields ...string) *AuthRole
// Save executes the query and returns the updated AuthRoles entity. // Save executes the query and returns the updated AuthRoles entity.
func (aruo *AuthRolesUpdateOne) Save(ctx context.Context) (*AuthRoles, error) { func (aruo *AuthRolesUpdateOne) Save(ctx context.Context) (*AuthRoles, error) {
return withHooks(ctx, aruo.sqlSave, aruo.mutation, aruo.hooks) return withHooks[*AuthRoles, AuthRolesMutation](ctx, aruo.sqlSave, aruo.mutation, aruo.hooks)
} }
// SaveX is like Save, but panics if an error occurs. // SaveX is like Save, but panics if an error occurs.
@ -308,7 +314,10 @@ func (aruo *AuthRolesUpdateOne) sqlSave(ctx context.Context) (_node *AuthRoles,
Columns: []string{authroles.TokenColumn}, Columns: []string{authroles.TokenColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authtokens.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: authtokens.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -321,7 +330,10 @@ func (aruo *AuthRolesUpdateOne) sqlSave(ctx context.Context) (_node *AuthRoles,
Columns: []string{authroles.TokenColumn}, Columns: []string{authroles.TokenColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authtokens.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: authtokens.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {

View file

@ -7,7 +7,6 @@ import (
"strings" "strings"
"time" "time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent/authroles" "github.com/hay-kot/homebox/backend/internal/data/ent/authroles"
@ -32,7 +31,6 @@ type AuthTokens struct {
// The values are being populated by the AuthTokensQuery when eager-loading is set. // The values are being populated by the AuthTokensQuery when eager-loading is set.
Edges AuthTokensEdges `json:"edges"` Edges AuthTokensEdges `json:"edges"`
user_auth_tokens *uuid.UUID user_auth_tokens *uuid.UUID
selectValues sql.SelectValues
} }
// AuthTokensEdges holds the relations/edges for other nodes in the graph. // AuthTokensEdges holds the relations/edges for other nodes in the graph.
@ -86,7 +84,7 @@ func (*AuthTokens) scanValues(columns []string) ([]any, error) {
case authtokens.ForeignKeys[0]: // user_auth_tokens case authtokens.ForeignKeys[0]: // user_auth_tokens
values[i] = &sql.NullScanner{S: new(uuid.UUID)} values[i] = &sql.NullScanner{S: new(uuid.UUID)}
default: default:
values[i] = new(sql.UnknownType) return nil, fmt.Errorf("unexpected column %q for type AuthTokens", columns[i])
} }
} }
return values, nil return values, nil
@ -137,19 +135,11 @@ func (at *AuthTokens) assignValues(columns []string, values []any) error {
at.user_auth_tokens = new(uuid.UUID) at.user_auth_tokens = new(uuid.UUID)
*at.user_auth_tokens = *value.S.(*uuid.UUID) *at.user_auth_tokens = *value.S.(*uuid.UUID)
} }
default:
at.selectValues.Set(columns[i], values[i])
} }
} }
return nil return nil
} }
// Value returns the ent.Value that was dynamically selected and assigned to the AuthTokens.
// This includes values selected through modifiers, order, etc.
func (at *AuthTokens) Value(name string) (ent.Value, error) {
return at.selectValues.Get(name)
}
// QueryUser queries the "user" edge of the AuthTokens entity. // QueryUser queries the "user" edge of the AuthTokens entity.
func (at *AuthTokens) QueryUser() *UserQuery { func (at *AuthTokens) QueryUser() *UserQuery {
return NewAuthTokensClient(at.config).QueryUser(at) return NewAuthTokensClient(at.config).QueryUser(at)

View file

@ -5,8 +5,6 @@ package authtokens
import ( import (
"time" "time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -87,54 +85,3 @@ var (
// DefaultID holds the default value on creation for the "id" field. // DefaultID holds the default value on creation for the "id" field.
DefaultID func() uuid.UUID DefaultID func() uuid.UUID
) )
// OrderOption defines the ordering options for the AuthTokens queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByCreatedAt orders the results by the created_at field.
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
}
// ByUpdatedAt orders the results by the updated_at field.
func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
}
// ByExpiresAt orders the results by the expires_at field.
func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldExpiresAt, opts...).ToFunc()
}
// ByUserField orders the results by user field.
func ByUserField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newUserStep(), sql.OrderByField(field, opts...))
}
}
// ByRolesField orders the results by roles field.
func ByRolesField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newRolesStep(), sql.OrderByField(field, opts...))
}
}
func newUserStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(UserInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn),
)
}
func newRolesStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(RolesInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, RolesTable, RolesColumn),
)
}

View file

@ -250,7 +250,11 @@ func HasUser() predicate.AuthTokens {
// HasUserWith applies the HasEdge predicate on the "user" edge with a given conditions (other predicates). // HasUserWith applies the HasEdge predicate on the "user" edge with a given conditions (other predicates).
func HasUserWith(preds ...predicate.User) predicate.AuthTokens { func HasUserWith(preds ...predicate.User) predicate.AuthTokens {
return predicate.AuthTokens(func(s *sql.Selector) { return predicate.AuthTokens(func(s *sql.Selector) {
step := newUserStep() step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(UserInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, UserTable, UserColumn),
)
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds { for _, p := range preds {
p(s) p(s)
@ -273,7 +277,11 @@ func HasRoles() predicate.AuthTokens {
// HasRolesWith applies the HasEdge predicate on the "roles" edge with a given conditions (other predicates). // HasRolesWith applies the HasEdge predicate on the "roles" edge with a given conditions (other predicates).
func HasRolesWith(preds ...predicate.AuthRoles) predicate.AuthTokens { func HasRolesWith(preds ...predicate.AuthRoles) predicate.AuthTokens {
return predicate.AuthTokens(func(s *sql.Selector) { return predicate.AuthTokens(func(s *sql.Selector) {
step := newRolesStep() step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(RolesInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2O, false, RolesTable, RolesColumn),
)
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds { for _, p := range preds {
p(s) p(s)

View file

@ -131,7 +131,7 @@ func (atc *AuthTokensCreate) Mutation() *AuthTokensMutation {
// Save creates the AuthTokens in the database. // Save creates the AuthTokens in the database.
func (atc *AuthTokensCreate) Save(ctx context.Context) (*AuthTokens, error) { func (atc *AuthTokensCreate) Save(ctx context.Context) (*AuthTokens, error) {
atc.defaults() atc.defaults()
return withHooks(ctx, atc.sqlSave, atc.mutation, atc.hooks) return withHooks[*AuthTokens, AuthTokensMutation](ctx, atc.sqlSave, atc.mutation, atc.hooks)
} }
// SaveX calls Save and panics if Save returns an error. // SaveX calls Save and panics if Save returns an error.
@ -249,7 +249,10 @@ func (atc *AuthTokensCreate) createSpec() (*AuthTokens, *sqlgraph.CreateSpec) {
Columns: []string{authtokens.UserColumn}, Columns: []string{authtokens.UserColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: user.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -266,7 +269,10 @@ func (atc *AuthTokensCreate) createSpec() (*AuthTokens, *sqlgraph.CreateSpec) {
Columns: []string{authtokens.RolesColumn}, Columns: []string{authtokens.RolesColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authroles.FieldID, field.TypeInt), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeInt,
Column: authroles.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -301,8 +307,8 @@ func (atcb *AuthTokensCreateBulk) Save(ctx context.Context) ([]*AuthTokens, erro
return nil, err return nil, err
} }
builder.mutation = mutation builder.mutation = mutation
var err error
nodes[i], specs[i] = builder.createSpec() nodes[i], specs[i] = builder.createSpec()
var err error
if i < len(mutators)-1 { if i < len(mutators)-1 {
_, err = mutators[i+1].Mutate(root, atcb.builders[i+1].mutation) _, err = mutators[i+1].Mutate(root, atcb.builders[i+1].mutation)
} else { } else {

View file

@ -27,7 +27,7 @@ func (atd *AuthTokensDelete) Where(ps ...predicate.AuthTokens) *AuthTokensDelete
// Exec executes the deletion query and returns how many vertices were deleted. // Exec executes the deletion query and returns how many vertices were deleted.
func (atd *AuthTokensDelete) Exec(ctx context.Context) (int, error) { func (atd *AuthTokensDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, atd.sqlExec, atd.mutation, atd.hooks) return withHooks[int, AuthTokensMutation](ctx, atd.sqlExec, atd.mutation, atd.hooks)
} }
// ExecX is like Exec, but panics if an error occurs. // ExecX is like Exec, but panics if an error occurs.

View file

@ -22,7 +22,7 @@ import (
type AuthTokensQuery struct { type AuthTokensQuery struct {
config config
ctx *QueryContext ctx *QueryContext
order []authtokens.OrderOption order []OrderFunc
inters []Interceptor inters []Interceptor
predicates []predicate.AuthTokens predicates []predicate.AuthTokens
withUser *UserQuery withUser *UserQuery
@ -59,7 +59,7 @@ func (atq *AuthTokensQuery) Unique(unique bool) *AuthTokensQuery {
} }
// Order specifies how the records should be ordered. // Order specifies how the records should be ordered.
func (atq *AuthTokensQuery) Order(o ...authtokens.OrderOption) *AuthTokensQuery { func (atq *AuthTokensQuery) Order(o ...OrderFunc) *AuthTokensQuery {
atq.order = append(atq.order, o...) atq.order = append(atq.order, o...)
return atq return atq
} }
@ -297,7 +297,7 @@ func (atq *AuthTokensQuery) Clone() *AuthTokensQuery {
return &AuthTokensQuery{ return &AuthTokensQuery{
config: atq.config, config: atq.config,
ctx: atq.ctx.Clone(), ctx: atq.ctx.Clone(),
order: append([]authtokens.OrderOption{}, atq.order...), order: append([]OrderFunc{}, atq.order...),
inters: append([]Interceptor{}, atq.inters...), inters: append([]Interceptor{}, atq.inters...),
predicates: append([]predicate.AuthTokens{}, atq.predicates...), predicates: append([]predicate.AuthTokens{}, atq.predicates...),
withUser: atq.withUser.Clone(), withUser: atq.withUser.Clone(),
@ -494,7 +494,7 @@ func (atq *AuthTokensQuery) loadRoles(ctx context.Context, query *AuthRolesQuery
} }
query.withFKs = true query.withFKs = true
query.Where(predicate.AuthRoles(func(s *sql.Selector) { query.Where(predicate.AuthRoles(func(s *sql.Selector) {
s.Where(sql.InValues(s.C(authtokens.RolesColumn), fks...)) s.Where(sql.InValues(authtokens.RolesColumn, fks...))
})) }))
neighbors, err := query.All(ctx) neighbors, err := query.All(ctx)
if err != nil { if err != nil {
@ -507,7 +507,7 @@ func (atq *AuthTokensQuery) loadRoles(ctx context.Context, query *AuthRolesQuery
} }
node, ok := nodeids[*fk] node, ok := nodeids[*fk]
if !ok { if !ok {
return fmt.Errorf(`unexpected referenced foreign-key "auth_tokens_roles" returned %v for node %v`, *fk, n.ID) return fmt.Errorf(`unexpected foreign-key "auth_tokens_roles" returned %v for node %v`, *fk, n.ID)
} }
assign(node, n) assign(node, n)
} }

View file

@ -115,7 +115,7 @@ func (atu *AuthTokensUpdate) ClearRoles() *AuthTokensUpdate {
// Save executes the query and returns the number of nodes affected by the update operation. // Save executes the query and returns the number of nodes affected by the update operation.
func (atu *AuthTokensUpdate) Save(ctx context.Context) (int, error) { func (atu *AuthTokensUpdate) Save(ctx context.Context) (int, error) {
atu.defaults() atu.defaults()
return withHooks(ctx, atu.sqlSave, atu.mutation, atu.hooks) return withHooks[int, AuthTokensMutation](ctx, atu.sqlSave, atu.mutation, atu.hooks)
} }
// SaveX is like Save, but panics if an error occurs. // SaveX is like Save, but panics if an error occurs.
@ -174,7 +174,10 @@ func (atu *AuthTokensUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{authtokens.UserColumn}, Columns: []string{authtokens.UserColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: user.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -187,7 +190,10 @@ func (atu *AuthTokensUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{authtokens.UserColumn}, Columns: []string{authtokens.UserColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: user.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -203,7 +209,10 @@ func (atu *AuthTokensUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{authtokens.RolesColumn}, Columns: []string{authtokens.RolesColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authroles.FieldID, field.TypeInt), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeInt,
Column: authroles.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -216,7 +225,10 @@ func (atu *AuthTokensUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{authtokens.RolesColumn}, Columns: []string{authtokens.RolesColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authroles.FieldID, field.TypeInt), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeInt,
Column: authroles.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -341,7 +353,7 @@ func (atuo *AuthTokensUpdateOne) Select(field string, fields ...string) *AuthTok
// Save executes the query and returns the updated AuthTokens entity. // Save executes the query and returns the updated AuthTokens entity.
func (atuo *AuthTokensUpdateOne) Save(ctx context.Context) (*AuthTokens, error) { func (atuo *AuthTokensUpdateOne) Save(ctx context.Context) (*AuthTokens, error) {
atuo.defaults() atuo.defaults()
return withHooks(ctx, atuo.sqlSave, atuo.mutation, atuo.hooks) return withHooks[*AuthTokens, AuthTokensMutation](ctx, atuo.sqlSave, atuo.mutation, atuo.hooks)
} }
// SaveX is like Save, but panics if an error occurs. // SaveX is like Save, but panics if an error occurs.
@ -417,7 +429,10 @@ func (atuo *AuthTokensUpdateOne) sqlSave(ctx context.Context) (_node *AuthTokens
Columns: []string{authtokens.UserColumn}, Columns: []string{authtokens.UserColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: user.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -430,7 +445,10 @@ func (atuo *AuthTokensUpdateOne) sqlSave(ctx context.Context) (_node *AuthTokens
Columns: []string{authtokens.UserColumn}, Columns: []string{authtokens.UserColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(user.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: user.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -446,7 +464,10 @@ func (atuo *AuthTokensUpdateOne) sqlSave(ctx context.Context) (_node *AuthTokens
Columns: []string{authtokens.RolesColumn}, Columns: []string{authtokens.RolesColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authroles.FieldID, field.TypeInt), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeInt,
Column: authroles.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -459,7 +480,10 @@ func (atuo *AuthTokensUpdateOne) sqlSave(ctx context.Context) (_node *AuthTokens
Columns: []string{authtokens.RolesColumn}, Columns: []string{authtokens.RolesColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(authroles.FieldID, field.TypeInt), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeInt,
Column: authroles.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {

View file

@ -11,10 +11,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent/migrate" "github.com/hay-kot/homebox/backend/internal/data/ent/migrate"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment" "github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/data/ent/authroles" "github.com/hay-kot/homebox/backend/internal/data/ent/authroles"
"github.com/hay-kot/homebox/backend/internal/data/ent/authtokens" "github.com/hay-kot/homebox/backend/internal/data/ent/authtokens"
@ -26,8 +22,11 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/ent/label" "github.com/hay-kot/homebox/backend/internal/data/ent/label"
"github.com/hay-kot/homebox/backend/internal/data/ent/location" "github.com/hay-kot/homebox/backend/internal/data/ent/location"
"github.com/hay-kot/homebox/backend/internal/data/ent/maintenanceentry" "github.com/hay-kot/homebox/backend/internal/data/ent/maintenanceentry"
"github.com/hay-kot/homebox/backend/internal/data/ent/notifier"
"github.com/hay-kot/homebox/backend/internal/data/ent/user" "github.com/hay-kot/homebox/backend/internal/data/ent/user"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
) )
// Client is the client that holds all ent builders. // Client is the client that holds all ent builders.
@ -57,8 +56,6 @@ type Client struct {
Location *LocationClient Location *LocationClient
// MaintenanceEntry is the client for interacting with the MaintenanceEntry builders. // MaintenanceEntry is the client for interacting with the MaintenanceEntry builders.
MaintenanceEntry *MaintenanceEntryClient MaintenanceEntry *MaintenanceEntryClient
// Notifier is the client for interacting with the Notifier builders.
Notifier *NotifierClient
// User is the client for interacting with the User builders. // User is the client for interacting with the User builders.
User *UserClient User *UserClient
} }
@ -85,59 +82,9 @@ func (c *Client) init() {
c.Label = NewLabelClient(c.config) c.Label = NewLabelClient(c.config)
c.Location = NewLocationClient(c.config) c.Location = NewLocationClient(c.config)
c.MaintenanceEntry = NewMaintenanceEntryClient(c.config) c.MaintenanceEntry = NewMaintenanceEntryClient(c.config)
c.Notifier = NewNotifierClient(c.config)
c.User = NewUserClient(c.config) c.User = NewUserClient(c.config)
} }
type (
// config is the configuration for the client and its builder.
config struct {
// driver used for executing database requests.
driver dialect.Driver
// debug enable a debug logging.
debug bool
// log used for logging on debug mode.
log func(...any)
// hooks to execute on mutations.
hooks *hooks
// interceptors to execute on queries.
inters *inters
}
// Option function to configure the client.
Option func(*config)
)
// options applies the options on the config object.
func (c *config) options(opts ...Option) {
for _, opt := range opts {
opt(c)
}
if c.debug {
c.driver = dialect.Debug(c.driver, c.log)
}
}
// Debug enables debug logging on the ent.Driver.
func Debug() Option {
return func(c *config) {
c.debug = true
}
}
// Log sets the logging function for debug mode.
func Log(fn func(...any)) Option {
return func(c *config) {
c.log = fn
}
}
// Driver configures the client driver.
func Driver(driver dialect.Driver) Option {
return func(c *config) {
c.driver = driver
}
}
// Open opens a database/sql.DB specified by the driver name and // Open opens a database/sql.DB specified by the driver name and
// the data source name, and returns a new client attached to it. // the data source name, and returns a new client attached to it.
// Optional parameters can be added for configuring the client. // Optional parameters can be added for configuring the client.
@ -180,7 +127,6 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
Label: NewLabelClient(cfg), Label: NewLabelClient(cfg),
Location: NewLocationClient(cfg), Location: NewLocationClient(cfg),
MaintenanceEntry: NewMaintenanceEntryClient(cfg), MaintenanceEntry: NewMaintenanceEntryClient(cfg),
Notifier: NewNotifierClient(cfg),
User: NewUserClient(cfg), User: NewUserClient(cfg),
}, nil }, nil
} }
@ -212,7 +158,6 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
Label: NewLabelClient(cfg), Label: NewLabelClient(cfg),
Location: NewLocationClient(cfg), Location: NewLocationClient(cfg),
MaintenanceEntry: NewMaintenanceEntryClient(cfg), MaintenanceEntry: NewMaintenanceEntryClient(cfg),
Notifier: NewNotifierClient(cfg),
User: NewUserClient(cfg), User: NewUserClient(cfg),
}, nil }, nil
} }
@ -242,25 +187,35 @@ func (c *Client) Close() error {
// Use adds the mutation hooks to all the entity clients. // Use adds the mutation hooks to all the entity clients.
// In order to add hooks to a specific client, call: `client.Node.Use(...)`. // In order to add hooks to a specific client, call: `client.Node.Use(...)`.
func (c *Client) Use(hooks ...Hook) { func (c *Client) Use(hooks ...Hook) {
for _, n := range []interface{ Use(...Hook) }{ c.Attachment.Use(hooks...)
c.Attachment, c.AuthRoles, c.AuthTokens, c.Document, c.Group, c.AuthRoles.Use(hooks...)
c.GroupInvitationToken, c.Item, c.ItemField, c.Label, c.Location, c.AuthTokens.Use(hooks...)
c.MaintenanceEntry, c.Notifier, c.User, c.Document.Use(hooks...)
} { c.Group.Use(hooks...)
n.Use(hooks...) c.GroupInvitationToken.Use(hooks...)
} c.Item.Use(hooks...)
c.ItemField.Use(hooks...)
c.Label.Use(hooks...)
c.Location.Use(hooks...)
c.MaintenanceEntry.Use(hooks...)
c.User.Use(hooks...)
} }
// Intercept adds the query interceptors to all the entity clients. // Intercept adds the query interceptors to all the entity clients.
// In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`. // In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`.
func (c *Client) Intercept(interceptors ...Interceptor) { func (c *Client) Intercept(interceptors ...Interceptor) {
for _, n := range []interface{ Intercept(...Interceptor) }{ c.Attachment.Intercept(interceptors...)
c.Attachment, c.AuthRoles, c.AuthTokens, c.Document, c.Group, c.AuthRoles.Intercept(interceptors...)
c.GroupInvitationToken, c.Item, c.ItemField, c.Label, c.Location, c.AuthTokens.Intercept(interceptors...)
c.MaintenanceEntry, c.Notifier, c.User, c.Document.Intercept(interceptors...)
} { c.Group.Intercept(interceptors...)
n.Intercept(interceptors...) c.GroupInvitationToken.Intercept(interceptors...)
} c.Item.Intercept(interceptors...)
c.ItemField.Intercept(interceptors...)
c.Label.Intercept(interceptors...)
c.Location.Intercept(interceptors...)
c.MaintenanceEntry.Intercept(interceptors...)
c.User.Intercept(interceptors...)
} }
// Mutate implements the ent.Mutator interface. // Mutate implements the ent.Mutator interface.
@ -288,8 +243,6 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
return c.Location.mutate(ctx, m) return c.Location.mutate(ctx, m)
case *MaintenanceEntryMutation: case *MaintenanceEntryMutation:
return c.MaintenanceEntry.mutate(ctx, m) return c.MaintenanceEntry.mutate(ctx, m)
case *NotifierMutation:
return c.Notifier.mutate(ctx, m)
case *UserMutation: case *UserMutation:
return c.User.mutate(ctx, m) return c.User.mutate(ctx, m)
default: default:
@ -1070,22 +1023,6 @@ func (c *GroupClient) QueryInvitationTokens(gr *Group) *GroupInvitationTokenQuer
return query return query
} }
// QueryNotifiers queries the notifiers edge of a Group.
func (c *GroupClient) QueryNotifiers(gr *Group) *NotifierQuery {
query := (&NotifierClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := gr.ID
step := sqlgraph.NewStep(
sqlgraph.From(group.Table, group.FieldID, id),
sqlgraph.To(notifier.Table, notifier.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, group.NotifiersTable, group.NotifiersColumn),
)
fromV = sqlgraph.Neighbors(gr.driver.Dialect(), step)
return fromV, nil
}
return query
}
// Hooks returns the client hooks. // Hooks returns the client hooks.
func (c *GroupClient) Hooks() []Hook { func (c *GroupClient) Hooks() []Hook {
return c.hooks.Group return c.hooks.Group
@ -1338,22 +1275,6 @@ func (c *ItemClient) GetX(ctx context.Context, id uuid.UUID) *Item {
return obj return obj
} }
// QueryGroup queries the group edge of a Item.
func (c *ItemClient) QueryGroup(i *Item) *GroupQuery {
query := (&GroupClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := i.ID
step := sqlgraph.NewStep(
sqlgraph.From(item.Table, item.FieldID, id),
sqlgraph.To(group.Table, group.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, item.GroupTable, item.GroupColumn),
)
fromV = sqlgraph.Neighbors(i.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryParent queries the parent edge of a Item. // QueryParent queries the parent edge of a Item.
func (c *ItemClient) QueryParent(i *Item) *ItemQuery { func (c *ItemClient) QueryParent(i *Item) *ItemQuery {
query := (&ItemClient{config: c.config}).Query() query := (&ItemClient{config: c.config}).Query()
@ -1386,6 +1307,22 @@ func (c *ItemClient) QueryChildren(i *Item) *ItemQuery {
return query return query
} }
// QueryGroup queries the group edge of a Item.
func (c *ItemClient) QueryGroup(i *Item) *GroupQuery {
query := (&GroupClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := i.ID
step := sqlgraph.NewStep(
sqlgraph.From(item.Table, item.FieldID, id),
sqlgraph.To(group.Table, group.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, item.GroupTable, item.GroupColumn),
)
fromV = sqlgraph.Neighbors(i.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryLabel queries the label edge of a Item. // QueryLabel queries the label edge of a Item.
func (c *ItemClient) QueryLabel(i *Item) *LabelQuery { func (c *ItemClient) QueryLabel(i *Item) *LabelQuery {
query := (&LabelClient{config: c.config}).Query() query := (&LabelClient{config: c.config}).Query()
@ -1868,22 +1805,6 @@ func (c *LocationClient) GetX(ctx context.Context, id uuid.UUID) *Location {
return obj return obj
} }
// QueryGroup queries the group edge of a Location.
func (c *LocationClient) QueryGroup(l *Location) *GroupQuery {
query := (&GroupClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := l.ID
step := sqlgraph.NewStep(
sqlgraph.From(location.Table, location.FieldID, id),
sqlgraph.To(group.Table, group.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, location.GroupTable, location.GroupColumn),
)
fromV = sqlgraph.Neighbors(l.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryParent queries the parent edge of a Location. // QueryParent queries the parent edge of a Location.
func (c *LocationClient) QueryParent(l *Location) *LocationQuery { func (c *LocationClient) QueryParent(l *Location) *LocationQuery {
query := (&LocationClient{config: c.config}).Query() query := (&LocationClient{config: c.config}).Query()
@ -1916,6 +1837,22 @@ func (c *LocationClient) QueryChildren(l *Location) *LocationQuery {
return query return query
} }
// QueryGroup queries the group edge of a Location.
func (c *LocationClient) QueryGroup(l *Location) *GroupQuery {
query := (&GroupClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := l.ID
step := sqlgraph.NewStep(
sqlgraph.From(location.Table, location.FieldID, id),
sqlgraph.To(group.Table, group.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, location.GroupTable, location.GroupColumn),
)
fromV = sqlgraph.Neighbors(l.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryItems queries the items edge of a Location. // QueryItems queries the items edge of a Location.
func (c *LocationClient) QueryItems(l *Location) *ItemQuery { func (c *LocationClient) QueryItems(l *Location) *ItemQuery {
query := (&ItemClient{config: c.config}).Query() query := (&ItemClient{config: c.config}).Query()
@ -2091,156 +2028,6 @@ func (c *MaintenanceEntryClient) mutate(ctx context.Context, m *MaintenanceEntry
} }
} }
// NotifierClient is a client for the Notifier schema.
type NotifierClient struct {
config
}
// NewNotifierClient returns a client for the Notifier from the given config.
func NewNotifierClient(c config) *NotifierClient {
return &NotifierClient{config: c}
}
// Use adds a list of mutation hooks to the hooks stack.
// A call to `Use(f, g, h)` equals to `notifier.Hooks(f(g(h())))`.
func (c *NotifierClient) Use(hooks ...Hook) {
c.hooks.Notifier = append(c.hooks.Notifier, hooks...)
}
// Intercept adds a list of query interceptors to the interceptors stack.
// A call to `Intercept(f, g, h)` equals to `notifier.Intercept(f(g(h())))`.
func (c *NotifierClient) Intercept(interceptors ...Interceptor) {
c.inters.Notifier = append(c.inters.Notifier, interceptors...)
}
// Create returns a builder for creating a Notifier entity.
func (c *NotifierClient) Create() *NotifierCreate {
mutation := newNotifierMutation(c.config, OpCreate)
return &NotifierCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// CreateBulk returns a builder for creating a bulk of Notifier entities.
func (c *NotifierClient) CreateBulk(builders ...*NotifierCreate) *NotifierCreateBulk {
return &NotifierCreateBulk{config: c.config, builders: builders}
}
// Update returns an update builder for Notifier.
func (c *NotifierClient) Update() *NotifierUpdate {
mutation := newNotifierMutation(c.config, OpUpdate)
return &NotifierUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOne returns an update builder for the given entity.
func (c *NotifierClient) UpdateOne(n *Notifier) *NotifierUpdateOne {
mutation := newNotifierMutation(c.config, OpUpdateOne, withNotifier(n))
return &NotifierUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// UpdateOneID returns an update builder for the given id.
func (c *NotifierClient) UpdateOneID(id uuid.UUID) *NotifierUpdateOne {
mutation := newNotifierMutation(c.config, OpUpdateOne, withNotifierID(id))
return &NotifierUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// Delete returns a delete builder for Notifier.
func (c *NotifierClient) Delete() *NotifierDelete {
mutation := newNotifierMutation(c.config, OpDelete)
return &NotifierDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
}
// DeleteOne returns a builder for deleting the given entity.
func (c *NotifierClient) DeleteOne(n *Notifier) *NotifierDeleteOne {
return c.DeleteOneID(n.ID)
}
// DeleteOneID returns a builder for deleting the given entity by its id.
func (c *NotifierClient) DeleteOneID(id uuid.UUID) *NotifierDeleteOne {
builder := c.Delete().Where(notifier.ID(id))
builder.mutation.id = &id
builder.mutation.op = OpDeleteOne
return &NotifierDeleteOne{builder}
}
// Query returns a query builder for Notifier.
func (c *NotifierClient) Query() *NotifierQuery {
return &NotifierQuery{
config: c.config,
ctx: &QueryContext{Type: TypeNotifier},
inters: c.Interceptors(),
}
}
// Get returns a Notifier entity by its id.
func (c *NotifierClient) Get(ctx context.Context, id uuid.UUID) (*Notifier, error) {
return c.Query().Where(notifier.ID(id)).Only(ctx)
}
// GetX is like Get, but panics if an error occurs.
func (c *NotifierClient) GetX(ctx context.Context, id uuid.UUID) *Notifier {
obj, err := c.Get(ctx, id)
if err != nil {
panic(err)
}
return obj
}
// QueryGroup queries the group edge of a Notifier.
func (c *NotifierClient) QueryGroup(n *Notifier) *GroupQuery {
query := (&GroupClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := n.ID
step := sqlgraph.NewStep(
sqlgraph.From(notifier.Table, notifier.FieldID, id),
sqlgraph.To(group.Table, group.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, notifier.GroupTable, notifier.GroupColumn),
)
fromV = sqlgraph.Neighbors(n.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryUser queries the user edge of a Notifier.
func (c *NotifierClient) QueryUser(n *Notifier) *UserQuery {
query := (&UserClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := n.ID
step := sqlgraph.NewStep(
sqlgraph.From(notifier.Table, notifier.FieldID, id),
sqlgraph.To(user.Table, user.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, notifier.UserTable, notifier.UserColumn),
)
fromV = sqlgraph.Neighbors(n.driver.Dialect(), step)
return fromV, nil
}
return query
}
// Hooks returns the client hooks.
func (c *NotifierClient) Hooks() []Hook {
return c.hooks.Notifier
}
// Interceptors returns the client interceptors.
func (c *NotifierClient) Interceptors() []Interceptor {
return c.inters.Notifier
}
func (c *NotifierClient) mutate(ctx context.Context, m *NotifierMutation) (Value, error) {
switch m.Op() {
case OpCreate:
return (&NotifierCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdate:
return (&NotifierUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpUpdateOne:
return (&NotifierUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
case OpDelete, OpDeleteOne:
return (&NotifierDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
default:
return nil, fmt.Errorf("ent: unknown Notifier mutation op: %q", m.Op())
}
}
// UserClient is a client for the User schema. // UserClient is a client for the User schema.
type UserClient struct { type UserClient struct {
config config
@ -2366,22 +2153,6 @@ func (c *UserClient) QueryAuthTokens(u *User) *AuthTokensQuery {
return query return query
} }
// QueryNotifiers queries the notifiers edge of a User.
func (c *UserClient) QueryNotifiers(u *User) *NotifierQuery {
query := (&NotifierClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := u.ID
step := sqlgraph.NewStep(
sqlgraph.From(user.Table, user.FieldID, id),
sqlgraph.To(notifier.Table, notifier.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, user.NotifiersTable, user.NotifiersColumn),
)
fromV = sqlgraph.Neighbors(u.driver.Dialect(), step)
return fromV, nil
}
return query
}
// Hooks returns the client hooks. // Hooks returns the client hooks.
func (c *UserClient) Hooks() []Hook { func (c *UserClient) Hooks() []Hook {
return c.hooks.User return c.hooks.User
@ -2406,15 +2177,3 @@ func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error)
return nil, fmt.Errorf("ent: unknown User mutation op: %q", m.Op()) return nil, fmt.Errorf("ent: unknown User mutation op: %q", m.Op())
} }
} }
// hooks and interceptors per client, for fast access.
type (
hooks struct {
Attachment, AuthRoles, AuthTokens, Document, Group, GroupInvitationToken, Item,
ItemField, Label, Location, MaintenanceEntry, Notifier, User []ent.Hook
}
inters struct {
Attachment, AuthRoles, AuthTokens, Document, Group, GroupInvitationToken, Item,
ItemField, Label, Location, MaintenanceEntry, Notifier, User []ent.Interceptor
}
)

View file

@ -0,0 +1,88 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"entgo.io/ent"
"entgo.io/ent/dialect"
)
// Option function to configure the client.
type Option func(*config)
// Config is the configuration for the client and its builder.
type config struct {
// driver used for executing database requests.
driver dialect.Driver
// debug enable a debug logging.
debug bool
// log used for logging on debug mode.
log func(...any)
// hooks to execute on mutations.
hooks *hooks
// interceptors to execute on queries.
inters *inters
}
// hooks and interceptors per client, for fast access.
type (
hooks struct {
Attachment []ent.Hook
AuthRoles []ent.Hook
AuthTokens []ent.Hook
Document []ent.Hook
Group []ent.Hook
GroupInvitationToken []ent.Hook
Item []ent.Hook
ItemField []ent.Hook
Label []ent.Hook
Location []ent.Hook
MaintenanceEntry []ent.Hook
User []ent.Hook
}
inters struct {
Attachment []ent.Interceptor
AuthRoles []ent.Interceptor
AuthTokens []ent.Interceptor
Document []ent.Interceptor
Group []ent.Interceptor
GroupInvitationToken []ent.Interceptor
Item []ent.Interceptor
ItemField []ent.Interceptor
Label []ent.Interceptor
Location []ent.Interceptor
MaintenanceEntry []ent.Interceptor
User []ent.Interceptor
}
)
// Options applies the options on the config object.
func (c *config) options(opts ...Option) {
for _, opt := range opts {
opt(c)
}
if c.debug {
c.driver = dialect.Debug(c.driver, c.log)
}
}
// Debug enables debug logging on the ent.Driver.
func Debug() Option {
return func(c *config) {
c.debug = true
}
}
// Log sets the logging function for debug mode.
func Log(fn func(...any)) Option {
return func(c *config) {
c.log = fn
}
}
// Driver configures the client driver.
func Driver(driver dialect.Driver) Option {
return func(c *config) {
c.driver = driver
}
}

View file

@ -0,0 +1,33 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"context"
)
type clientCtxKey struct{}
// FromContext returns a Client stored inside a context, or nil if there isn't one.
func FromContext(ctx context.Context) *Client {
c, _ := ctx.Value(clientCtxKey{}).(*Client)
return c
}
// NewContext returns a new context with the given Client attached.
func NewContext(parent context.Context, c *Client) context.Context {
return context.WithValue(parent, clientCtxKey{}, c)
}
type txCtxKey struct{}
// TxFromContext returns a Tx stored inside a context, or nil if there isn't one.
func TxFromContext(ctx context.Context) *Tx {
tx, _ := ctx.Value(txCtxKey{}).(*Tx)
return tx
}
// NewTxContext returns a new context with the given Tx attached.
func NewTxContext(parent context.Context, tx *Tx) context.Context {
return context.WithValue(parent, txCtxKey{}, tx)
}

View file

@ -7,7 +7,6 @@ import (
"strings" "strings"
"time" "time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent/document" "github.com/hay-kot/homebox/backend/internal/data/ent/document"
@ -31,7 +30,6 @@ type Document struct {
// The values are being populated by the DocumentQuery when eager-loading is set. // The values are being populated by the DocumentQuery when eager-loading is set.
Edges DocumentEdges `json:"edges"` Edges DocumentEdges `json:"edges"`
group_documents *uuid.UUID group_documents *uuid.UUID
selectValues sql.SelectValues
} }
// DocumentEdges holds the relations/edges for other nodes in the graph. // DocumentEdges holds the relations/edges for other nodes in the graph.
@ -81,7 +79,7 @@ func (*Document) scanValues(columns []string) ([]any, error) {
case document.ForeignKeys[0]: // group_documents case document.ForeignKeys[0]: // group_documents
values[i] = &sql.NullScanner{S: new(uuid.UUID)} values[i] = &sql.NullScanner{S: new(uuid.UUID)}
default: default:
values[i] = new(sql.UnknownType) return nil, fmt.Errorf("unexpected column %q for type Document", columns[i])
} }
} }
return values, nil return values, nil
@ -132,19 +130,11 @@ func (d *Document) assignValues(columns []string, values []any) error {
d.group_documents = new(uuid.UUID) d.group_documents = new(uuid.UUID)
*d.group_documents = *value.S.(*uuid.UUID) *d.group_documents = *value.S.(*uuid.UUID)
} }
default:
d.selectValues.Set(columns[i], values[i])
} }
} }
return nil return nil
} }
// Value returns the ent.Value that was dynamically selected and assigned to the Document.
// This includes values selected through modifiers, order, etc.
func (d *Document) Value(name string) (ent.Value, error) {
return d.selectValues.Get(name)
}
// QueryGroup queries the "group" edge of the Document entity. // QueryGroup queries the "group" edge of the Document entity.
func (d *Document) QueryGroup() *GroupQuery { func (d *Document) QueryGroup() *GroupQuery {
return NewDocumentClient(d.config).QueryGroup(d) return NewDocumentClient(d.config).QueryGroup(d)

View file

@ -5,8 +5,6 @@ package document
import ( import (
"time" "time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -89,66 +87,3 @@ var (
// DefaultID holds the default value on creation for the "id" field. // DefaultID holds the default value on creation for the "id" field.
DefaultID func() uuid.UUID DefaultID func() uuid.UUID
) )
// OrderOption defines the ordering options for the Document queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByCreatedAt orders the results by the created_at field.
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
}
// ByUpdatedAt orders the results by the updated_at field.
func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
}
// ByTitle orders the results by the title field.
func ByTitle(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldTitle, opts...).ToFunc()
}
// ByPath orders the results by the path field.
func ByPath(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldPath, opts...).ToFunc()
}
// ByGroupField orders the results by group field.
func ByGroupField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newGroupStep(), sql.OrderByField(field, opts...))
}
}
// ByAttachmentsCount orders the results by attachments count.
func ByAttachmentsCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborsCount(s, newAttachmentsStep(), opts...)
}
}
// ByAttachments orders the results by attachments terms.
func ByAttachments(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newAttachmentsStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
func newGroupStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(GroupInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, GroupTable, GroupColumn),
)
}
func newAttachmentsStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(AttachmentsInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, AttachmentsTable, AttachmentsColumn),
)
}

View file

@ -300,7 +300,11 @@ func HasGroup() predicate.Document {
// HasGroupWith applies the HasEdge predicate on the "group" edge with a given conditions (other predicates). // HasGroupWith applies the HasEdge predicate on the "group" edge with a given conditions (other predicates).
func HasGroupWith(preds ...predicate.Group) predicate.Document { func HasGroupWith(preds ...predicate.Group) predicate.Document {
return predicate.Document(func(s *sql.Selector) { return predicate.Document(func(s *sql.Selector) {
step := newGroupStep() step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(GroupInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, GroupTable, GroupColumn),
)
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds { for _, p := range preds {
p(s) p(s)
@ -323,7 +327,11 @@ func HasAttachments() predicate.Document {
// HasAttachmentsWith applies the HasEdge predicate on the "attachments" edge with a given conditions (other predicates). // HasAttachmentsWith applies the HasEdge predicate on the "attachments" edge with a given conditions (other predicates).
func HasAttachmentsWith(preds ...predicate.Attachment) predicate.Document { func HasAttachmentsWith(preds ...predicate.Attachment) predicate.Document {
return predicate.Document(func(s *sql.Selector) { return predicate.Document(func(s *sql.Selector) {
step := newAttachmentsStep() step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(AttachmentsInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, AttachmentsTable, AttachmentsColumn),
)
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds { for _, p := range preds {
p(s) p(s)

View file

@ -111,7 +111,7 @@ func (dc *DocumentCreate) Mutation() *DocumentMutation {
// Save creates the Document in the database. // Save creates the Document in the database.
func (dc *DocumentCreate) Save(ctx context.Context) (*Document, error) { func (dc *DocumentCreate) Save(ctx context.Context) (*Document, error) {
dc.defaults() dc.defaults()
return withHooks(ctx, dc.sqlSave, dc.mutation, dc.hooks) return withHooks[*Document, DocumentMutation](ctx, dc.sqlSave, dc.mutation, dc.hooks)
} }
// SaveX calls Save and panics if Save returns an error. // SaveX calls Save and panics if Save returns an error.
@ -238,7 +238,10 @@ func (dc *DocumentCreate) createSpec() (*Document, *sqlgraph.CreateSpec) {
Columns: []string{document.GroupColumn}, Columns: []string{document.GroupColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: group.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -255,7 +258,10 @@ func (dc *DocumentCreate) createSpec() (*Document, *sqlgraph.CreateSpec) {
Columns: []string{document.AttachmentsColumn}, Columns: []string{document.AttachmentsColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: attachment.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -290,8 +296,8 @@ func (dcb *DocumentCreateBulk) Save(ctx context.Context) ([]*Document, error) {
return nil, err return nil, err
} }
builder.mutation = mutation builder.mutation = mutation
var err error
nodes[i], specs[i] = builder.createSpec() nodes[i], specs[i] = builder.createSpec()
var err error
if i < len(mutators)-1 { if i < len(mutators)-1 {
_, err = mutators[i+1].Mutate(root, dcb.builders[i+1].mutation) _, err = mutators[i+1].Mutate(root, dcb.builders[i+1].mutation)
} else { } else {

View file

@ -27,7 +27,7 @@ func (dd *DocumentDelete) Where(ps ...predicate.Document) *DocumentDelete {
// Exec executes the deletion query and returns how many vertices were deleted. // Exec executes the deletion query and returns how many vertices were deleted.
func (dd *DocumentDelete) Exec(ctx context.Context) (int, error) { func (dd *DocumentDelete) Exec(ctx context.Context) (int, error) {
return withHooks(ctx, dd.sqlExec, dd.mutation, dd.hooks) return withHooks[int, DocumentMutation](ctx, dd.sqlExec, dd.mutation, dd.hooks)
} }
// ExecX is like Exec, but panics if an error occurs. // ExecX is like Exec, but panics if an error occurs.

View file

@ -22,7 +22,7 @@ import (
type DocumentQuery struct { type DocumentQuery struct {
config config
ctx *QueryContext ctx *QueryContext
order []document.OrderOption order []OrderFunc
inters []Interceptor inters []Interceptor
predicates []predicate.Document predicates []predicate.Document
withGroup *GroupQuery withGroup *GroupQuery
@ -59,7 +59,7 @@ func (dq *DocumentQuery) Unique(unique bool) *DocumentQuery {
} }
// Order specifies how the records should be ordered. // Order specifies how the records should be ordered.
func (dq *DocumentQuery) Order(o ...document.OrderOption) *DocumentQuery { func (dq *DocumentQuery) Order(o ...OrderFunc) *DocumentQuery {
dq.order = append(dq.order, o...) dq.order = append(dq.order, o...)
return dq return dq
} }
@ -297,7 +297,7 @@ func (dq *DocumentQuery) Clone() *DocumentQuery {
return &DocumentQuery{ return &DocumentQuery{
config: dq.config, config: dq.config,
ctx: dq.ctx.Clone(), ctx: dq.ctx.Clone(),
order: append([]document.OrderOption{}, dq.order...), order: append([]OrderFunc{}, dq.order...),
inters: append([]Interceptor{}, dq.inters...), inters: append([]Interceptor{}, dq.inters...),
predicates: append([]predicate.Document{}, dq.predicates...), predicates: append([]predicate.Document{}, dq.predicates...),
withGroup: dq.withGroup.Clone(), withGroup: dq.withGroup.Clone(),
@ -498,7 +498,7 @@ func (dq *DocumentQuery) loadAttachments(ctx context.Context, query *AttachmentQ
} }
query.withFKs = true query.withFKs = true
query.Where(predicate.Attachment(func(s *sql.Selector) { query.Where(predicate.Attachment(func(s *sql.Selector) {
s.Where(sql.InValues(s.C(document.AttachmentsColumn), fks...)) s.Where(sql.InValues(document.AttachmentsColumn, fks...))
})) }))
neighbors, err := query.All(ctx) neighbors, err := query.All(ctx)
if err != nil { if err != nil {
@ -511,7 +511,7 @@ func (dq *DocumentQuery) loadAttachments(ctx context.Context, query *AttachmentQ
} }
node, ok := nodeids[*fk] node, ok := nodeids[*fk]
if !ok { if !ok {
return fmt.Errorf(`unexpected referenced foreign-key "document_attachments" returned %v for node %v`, *fk, n.ID) return fmt.Errorf(`unexpected foreign-key "document_attachments" returned %v for node %v`, *fk, n.ID)
} }
assign(node, n) assign(node, n)
} }

View file

@ -110,7 +110,7 @@ func (du *DocumentUpdate) RemoveAttachments(a ...*Attachment) *DocumentUpdate {
// Save executes the query and returns the number of nodes affected by the update operation. // Save executes the query and returns the number of nodes affected by the update operation.
func (du *DocumentUpdate) Save(ctx context.Context) (int, error) { func (du *DocumentUpdate) Save(ctx context.Context) (int, error) {
du.defaults() du.defaults()
return withHooks(ctx, du.sqlSave, du.mutation, du.hooks) return withHooks[int, DocumentMutation](ctx, du.sqlSave, du.mutation, du.hooks)
} }
// SaveX is like Save, but panics if an error occurs. // SaveX is like Save, but panics if an error occurs.
@ -190,7 +190,10 @@ func (du *DocumentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{document.GroupColumn}, Columns: []string{document.GroupColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: group.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -203,7 +206,10 @@ func (du *DocumentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{document.GroupColumn}, Columns: []string{document.GroupColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: group.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -219,7 +225,10 @@ func (du *DocumentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{document.AttachmentsColumn}, Columns: []string{document.AttachmentsColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: attachment.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -232,7 +241,10 @@ func (du *DocumentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{document.AttachmentsColumn}, Columns: []string{document.AttachmentsColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: attachment.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -248,7 +260,10 @@ func (du *DocumentUpdate) sqlSave(ctx context.Context) (n int, err error) {
Columns: []string{document.AttachmentsColumn}, Columns: []string{document.AttachmentsColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: attachment.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -368,7 +383,7 @@ func (duo *DocumentUpdateOne) Select(field string, fields ...string) *DocumentUp
// Save executes the query and returns the updated Document entity. // Save executes the query and returns the updated Document entity.
func (duo *DocumentUpdateOne) Save(ctx context.Context) (*Document, error) { func (duo *DocumentUpdateOne) Save(ctx context.Context) (*Document, error) {
duo.defaults() duo.defaults()
return withHooks(ctx, duo.sqlSave, duo.mutation, duo.hooks) return withHooks[*Document, DocumentMutation](ctx, duo.sqlSave, duo.mutation, duo.hooks)
} }
// SaveX is like Save, but panics if an error occurs. // SaveX is like Save, but panics if an error occurs.
@ -465,7 +480,10 @@ func (duo *DocumentUpdateOne) sqlSave(ctx context.Context) (_node *Document, err
Columns: []string{document.GroupColumn}, Columns: []string{document.GroupColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: group.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -478,7 +496,10 @@ func (duo *DocumentUpdateOne) sqlSave(ctx context.Context) (_node *Document, err
Columns: []string{document.GroupColumn}, Columns: []string{document.GroupColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(group.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: group.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -494,7 +515,10 @@ func (duo *DocumentUpdateOne) sqlSave(ctx context.Context) (_node *Document, err
Columns: []string{document.AttachmentsColumn}, Columns: []string{document.AttachmentsColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: attachment.FieldID,
},
}, },
} }
_spec.Edges.Clear = append(_spec.Edges.Clear, edge) _spec.Edges.Clear = append(_spec.Edges.Clear, edge)
@ -507,7 +531,10 @@ func (duo *DocumentUpdateOne) sqlSave(ctx context.Context) (_node *Document, err
Columns: []string{document.AttachmentsColumn}, Columns: []string{document.AttachmentsColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: attachment.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {
@ -523,7 +550,10 @@ func (duo *DocumentUpdateOne) sqlSave(ctx context.Context) (_node *Document, err
Columns: []string{document.AttachmentsColumn}, Columns: []string{document.AttachmentsColumn},
Bidi: false, Bidi: false,
Target: &sqlgraph.EdgeTarget{ Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(attachment.FieldID, field.TypeUUID), IDSpec: &sqlgraph.FieldSpec{
Type: field.TypeUUID,
Column: attachment.FieldID,
},
}, },
} }
for _, k := range nodes { for _, k := range nodes {

View file

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"sync"
"entgo.io/ent" "entgo.io/ent"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
@ -23,7 +22,6 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/ent/label" "github.com/hay-kot/homebox/backend/internal/data/ent/label"
"github.com/hay-kot/homebox/backend/internal/data/ent/location" "github.com/hay-kot/homebox/backend/internal/data/ent/location"
"github.com/hay-kot/homebox/backend/internal/data/ent/maintenanceentry" "github.com/hay-kot/homebox/backend/internal/data/ent/maintenanceentry"
"github.com/hay-kot/homebox/backend/internal/data/ent/notifier"
"github.com/hay-kot/homebox/backend/internal/data/ent/user" "github.com/hay-kot/homebox/backend/internal/data/ent/user"
) )
@ -46,45 +44,12 @@ type (
MutateFunc = ent.MutateFunc MutateFunc = ent.MutateFunc
) )
type clientCtxKey struct{}
// FromContext returns a Client stored inside a context, or nil if there isn't one.
func FromContext(ctx context.Context) *Client {
c, _ := ctx.Value(clientCtxKey{}).(*Client)
return c
}
// NewContext returns a new context with the given Client attached.
func NewContext(parent context.Context, c *Client) context.Context {
return context.WithValue(parent, clientCtxKey{}, c)
}
type txCtxKey struct{}
// TxFromContext returns a Tx stored inside a context, or nil if there isn't one.
func TxFromContext(ctx context.Context) *Tx {
tx, _ := ctx.Value(txCtxKey{}).(*Tx)
return tx
}
// NewTxContext returns a new context with the given Tx attached.
func NewTxContext(parent context.Context, tx *Tx) context.Context {
return context.WithValue(parent, txCtxKey{}, tx)
}
// OrderFunc applies an ordering on the sql selector. // OrderFunc applies an ordering on the sql selector.
// Deprecated: Use Asc/Desc functions or the package builders instead.
type OrderFunc func(*sql.Selector) type OrderFunc func(*sql.Selector)
var ( // columnChecker returns a function indicates if the column exists in the given column.
initCheck sync.Once func columnChecker(table string) func(string) error {
columnCheck sql.ColumnCheck checks := map[string]func(string) bool{
)
// columnChecker checks if the column exists in the given table.
func checkColumn(table, column string) error {
initCheck.Do(func() {
columnCheck = sql.NewColumnCheck(map[string]func(string) bool{
attachment.Table: attachment.ValidColumn, attachment.Table: attachment.ValidColumn,
authroles.Table: authroles.ValidColumn, authroles.Table: authroles.ValidColumn,
authtokens.Table: authtokens.ValidColumn, authtokens.Table: authtokens.ValidColumn,
@ -96,18 +61,28 @@ func checkColumn(table, column string) error {
label.Table: label.ValidColumn, label.Table: label.ValidColumn,
location.Table: location.ValidColumn, location.Table: location.ValidColumn,
maintenanceentry.Table: maintenanceentry.ValidColumn, maintenanceentry.Table: maintenanceentry.ValidColumn,
notifier.Table: notifier.ValidColumn,
user.Table: user.ValidColumn, user.Table: user.ValidColumn,
}) }
}) check, ok := checks[table]
return columnCheck(table, column) if !ok {
return func(string) error {
return fmt.Errorf("unknown table %q", table)
}
}
return func(column string) error {
if !check(column) {
return fmt.Errorf("unknown column %q for table %q", column, table)
}
return nil
}
} }
// Asc applies the given fields in ASC order. // Asc applies the given fields in ASC order.
func Asc(fields ...string) func(*sql.Selector) { func Asc(fields ...string) OrderFunc {
return func(s *sql.Selector) { return func(s *sql.Selector) {
check := columnChecker(s.TableName())
for _, f := range fields { for _, f := range fields {
if err := checkColumn(s.TableName(), f); err != nil { if err := check(f); err != nil {
s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)}) s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)})
} }
s.OrderBy(sql.Asc(s.C(f))) s.OrderBy(sql.Asc(s.C(f)))
@ -116,10 +91,11 @@ func Asc(fields ...string) func(*sql.Selector) {
} }
// Desc applies the given fields in DESC order. // Desc applies the given fields in DESC order.
func Desc(fields ...string) func(*sql.Selector) { func Desc(fields ...string) OrderFunc {
return func(s *sql.Selector) { return func(s *sql.Selector) {
check := columnChecker(s.TableName())
for _, f := range fields { for _, f := range fields {
if err := checkColumn(s.TableName(), f); err != nil { if err := check(f); err != nil {
s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)}) s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)})
} }
s.OrderBy(sql.Desc(s.C(f))) s.OrderBy(sql.Desc(s.C(f)))
@ -151,7 +127,8 @@ func Count() AggregateFunc {
// Max applies the "max" aggregation function on the given field of each group. // Max applies the "max" aggregation function on the given field of each group.
func Max(field string) AggregateFunc { func Max(field string) AggregateFunc {
return func(s *sql.Selector) string { return func(s *sql.Selector) string {
if err := checkColumn(s.TableName(), field); err != nil { check := columnChecker(s.TableName())
if err := check(field); err != nil {
s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)})
return "" return ""
} }
@ -162,7 +139,8 @@ func Max(field string) AggregateFunc {
// Mean applies the "mean" aggregation function on the given field of each group. // Mean applies the "mean" aggregation function on the given field of each group.
func Mean(field string) AggregateFunc { func Mean(field string) AggregateFunc {
return func(s *sql.Selector) string { return func(s *sql.Selector) string {
if err := checkColumn(s.TableName(), field); err != nil { check := columnChecker(s.TableName())
if err := check(field); err != nil {
s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)})
return "" return ""
} }
@ -173,7 +151,8 @@ func Mean(field string) AggregateFunc {
// Min applies the "min" aggregation function on the given field of each group. // Min applies the "min" aggregation function on the given field of each group.
func Min(field string) AggregateFunc { func Min(field string) AggregateFunc {
return func(s *sql.Selector) string { return func(s *sql.Selector) string {
if err := checkColumn(s.TableName(), field); err != nil { check := columnChecker(s.TableName())
if err := check(field); err != nil {
s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)})
return "" return ""
} }
@ -184,7 +163,8 @@ func Min(field string) AggregateFunc {
// Sum applies the "sum" aggregation function on the given field of each group. // Sum applies the "sum" aggregation function on the given field of each group.
func Sum(field string) AggregateFunc { func Sum(field string) AggregateFunc {
return func(s *sql.Selector) string { return func(s *sql.Selector) string {
if err := checkColumn(s.TableName(), field); err != nil { check := columnChecker(s.TableName())
if err := check(field); err != nil {
s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)})
return "" return ""
} }
@ -521,7 +501,7 @@ func withHooks[V Value, M any, PM interface {
return exec(ctx) return exec(ctx)
} }
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
mutationT, ok := any(m).(PM) mutationT, ok := m.(PM)
if !ok { if !ok {
return nil, fmt.Errorf("unexpected mutation type %T", m) return nil, fmt.Errorf("unexpected mutation type %T", m)
} }

View file

@ -7,7 +7,6 @@ import (
"strings" "strings"
"time" "time"
"entgo.io/ent"
"entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/group"
@ -29,7 +28,6 @@ type Group struct {
// Edges holds the relations/edges for other nodes in the graph. // Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the GroupQuery when eager-loading is set. // The values are being populated by the GroupQuery when eager-loading is set.
Edges GroupEdges `json:"edges"` Edges GroupEdges `json:"edges"`
selectValues sql.SelectValues
} }
// GroupEdges holds the relations/edges for other nodes in the graph. // GroupEdges holds the relations/edges for other nodes in the graph.
@ -46,11 +44,9 @@ type GroupEdges struct {
Documents []*Document `json:"documents,omitempty"` Documents []*Document `json:"documents,omitempty"`
// InvitationTokens holds the value of the invitation_tokens edge. // InvitationTokens holds the value of the invitation_tokens edge.
InvitationTokens []*GroupInvitationToken `json:"invitation_tokens,omitempty"` InvitationTokens []*GroupInvitationToken `json:"invitation_tokens,omitempty"`
// Notifiers holds the value of the notifiers edge.
Notifiers []*Notifier `json:"notifiers,omitempty"`
// loadedTypes holds the information for reporting if a // loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not. // type was loaded (or requested) in eager-loading or not.
loadedTypes [7]bool loadedTypes [6]bool
} }
// UsersOrErr returns the Users value or an error if the edge // UsersOrErr returns the Users value or an error if the edge
@ -107,15 +103,6 @@ func (e GroupEdges) InvitationTokensOrErr() ([]*GroupInvitationToken, error) {
return nil, &NotLoadedError{edge: "invitation_tokens"} return nil, &NotLoadedError{edge: "invitation_tokens"}
} }
// NotifiersOrErr returns the Notifiers value or an error if the edge
// was not loaded in eager-loading.
func (e GroupEdges) NotifiersOrErr() ([]*Notifier, error) {
if e.loadedTypes[6] {
return e.Notifiers, nil
}
return nil, &NotLoadedError{edge: "notifiers"}
}
// scanValues returns the types for scanning values from sql.Rows. // scanValues returns the types for scanning values from sql.Rows.
func (*Group) scanValues(columns []string) ([]any, error) { func (*Group) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns)) values := make([]any, len(columns))
@ -128,7 +115,7 @@ func (*Group) scanValues(columns []string) ([]any, error) {
case group.FieldID: case group.FieldID:
values[i] = new(uuid.UUID) values[i] = new(uuid.UUID)
default: default:
values[i] = new(sql.UnknownType) return nil, fmt.Errorf("unexpected column %q for type Group", columns[i])
} }
} }
return values, nil return values, nil
@ -172,19 +159,11 @@ func (gr *Group) assignValues(columns []string, values []any) error {
} else if value.Valid { } else if value.Valid {
gr.Currency = group.Currency(value.String) gr.Currency = group.Currency(value.String)
} }
default:
gr.selectValues.Set(columns[i], values[i])
} }
} }
return nil return nil
} }
// Value returns the ent.Value that was dynamically selected and assigned to the Group.
// This includes values selected through modifiers, order, etc.
func (gr *Group) Value(name string) (ent.Value, error) {
return gr.selectValues.Get(name)
}
// QueryUsers queries the "users" edge of the Group entity. // QueryUsers queries the "users" edge of the Group entity.
func (gr *Group) QueryUsers() *UserQuery { func (gr *Group) QueryUsers() *UserQuery {
return NewGroupClient(gr.config).QueryUsers(gr) return NewGroupClient(gr.config).QueryUsers(gr)
@ -215,11 +194,6 @@ func (gr *Group) QueryInvitationTokens() *GroupInvitationTokenQuery {
return NewGroupClient(gr.config).QueryInvitationTokens(gr) return NewGroupClient(gr.config).QueryInvitationTokens(gr)
} }
// QueryNotifiers queries the "notifiers" edge of the Group entity.
func (gr *Group) QueryNotifiers() *NotifierQuery {
return NewGroupClient(gr.config).QueryNotifiers(gr)
}
// Update returns a builder for updating this Group. // Update returns a builder for updating this Group.
// Note that you need to call Group.Unwrap() before calling this method if this Group // Note that you need to call Group.Unwrap() before calling this method if this Group
// was returned from a transaction, and the transaction was committed or rolled back. // was returned from a transaction, and the transaction was committed or rolled back.

Some files were not shown because too many files have changed in this diff Show more