diff --git a/backend/app/api/handlers/v1/controller.go b/backend/app/api/handlers/v1/controller.go index ff03bfe..718634a 100644 --- a/backend/app/api/handlers/v1/controller.go +++ b/backend/app/api/handlers/v1/controller.go @@ -9,6 +9,14 @@ import ( "github.com/hay-kot/safeserve/server" ) +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"` } diff --git a/backend/app/api/handlers/v1/v1_ctrl_labels.go b/backend/app/api/handlers/v1/v1_ctrl_labels.go index 0dabe6f..e21036d 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_labels.go +++ b/backend/app/api/handlers/v1/v1_ctrl_labels.go @@ -15,7 +15,7 @@ import ( // @Summary Get All Labels // @Tags Labels // @Produce json -// @Success 200 {object} Wrapped{items=[]repo.LabelOut} +// @Success 200 {object} []repo.LabelOut // @Router /v1/labels [GET] // @Security Bearer func (ctrl *V1Controller) HandleLabelsGetAll() errchain.HandlerFunc { diff --git a/backend/app/api/handlers/v1/v1_ctrl_locations.go b/backend/app/api/handlers/v1/v1_ctrl_locations.go index b495c66..c9ffa57 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_locations.go +++ b/backend/app/api/handlers/v1/v1_ctrl_locations.go @@ -16,7 +16,7 @@ import ( // @Tags Locations // @Produce json // @Param withItems query bool false "include items in response tree" -// @Success 200 {object} Wrapped{items=[]repo.TreeItem} +// @Success 200 {object} []repo.TreeItem // @Router /v1/locations/tree [GET] // @Security Bearer func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc { @@ -34,7 +34,7 @@ func (ctrl *V1Controller) HandleLocationTreeQuery() errchain.HandlerFunc { // @Tags Locations // @Produce json // @Param filterChildren query bool false "Filter locations with parents" -// @Success 200 {object} Wrapped{items=[]repo.LocationOutCount} +// @Success 200 {object} []repo.LocationOutCount // @Router /v1/locations [GET] // @Security Bearer func (ctrl *V1Controller) HandleLocationGetAll() errchain.HandlerFunc { diff --git a/backend/app/api/handlers/v1/v1_ctrl_notifiers.go b/backend/app/api/handlers/v1/v1_ctrl_notifiers.go index 51da4d7..da561c4 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_notifiers.go +++ b/backend/app/api/handlers/v1/v1_ctrl_notifiers.go @@ -16,7 +16,7 @@ import ( // @Summary Get Notifiers // @Tags Notifiers // @Produce json -// @Success 200 {object} Wrapped{items=[]repo.NotifierOut} +// @Success 200 {object} []repo.NotifierOut // @Router /v1/notifiers [GET] // @Security Bearer func (ctrl *V1Controller) HandleGetUserNotifiers() errchain.HandlerFunc { diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 5ad614f..8841610 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -945,22 +945,10 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LabelOut" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.LabelOut" + } } } } @@ -1117,22 +1105,10 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LocationOutCount" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.LocationOutCount" + } } } } @@ -1197,22 +1173,10 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.TreeItem" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.TreeItem" + } } } } @@ -1337,22 +1301,10 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.NotifierOut" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.NotifierOut" + } } } } @@ -1917,14 +1869,12 @@ const docTemplate = `{ "repo.ItemCreate": { "type": "object", "required": [ - "description", "name" ], "properties": { "description": { "type": "string", - "maxLength": 1000, - "minLength": 1 + "maxLength": 1000 }, "labelIds": { "type": "array", @@ -2433,6 +2383,9 @@ const docTemplate = `{ }, "repo.MaintenanceEntryCreate": { "type": "object", + "required": [ + "name" + ], "properties": { "completedDate": { "description": "Sold", diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index c12bd42..0eaa5f4 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -937,22 +937,10 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LabelOut" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.LabelOut" + } } } } @@ -1109,22 +1097,10 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LocationOutCount" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.LocationOutCount" + } } } } @@ -1189,22 +1165,10 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.TreeItem" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.TreeItem" + } } } } @@ -1329,22 +1293,10 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.NotifierOut" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.NotifierOut" + } } } } @@ -1909,14 +1861,12 @@ "repo.ItemCreate": { "type": "object", "required": [ - "description", "name" ], "properties": { "description": { "type": "string", - "maxLength": 1000, - "minLength": 1 + "maxLength": 1000 }, "labelIds": { "type": "array", @@ -2425,6 +2375,9 @@ }, "repo.MaintenanceEntryCreate": { "type": "object", + "required": [ + "name" + ], "properties": { "completedDate": { "description": "Sold", diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index 8a8b7f7..5205c69 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -77,7 +77,6 @@ definitions: properties: description: maxLength: 1000 - minLength: 1 type: string labelIds: items: @@ -94,7 +93,6 @@ definitions: type: string x-nullable: true required: - - description - name type: object repo.ItemField: @@ -442,6 +440,8 @@ definitions: scheduledDate: description: Sold type: string + required: + - name type: object repo.MaintenanceEntryUpdate: properties: @@ -1258,14 +1258,9 @@ paths: "200": description: OK schema: - allOf: - - $ref: '#/definitions/v1.Wrapped' - - properties: - items: - items: - $ref: '#/definitions/repo.LabelOut' - type: array - type: object + items: + $ref: '#/definitions/repo.LabelOut' + type: array security: - Bearer: [] summary: Get All Labels @@ -1360,14 +1355,9 @@ paths: "200": description: OK schema: - allOf: - - $ref: '#/definitions/v1.Wrapped' - - properties: - items: - items: - $ref: '#/definitions/repo.LocationOutCount' - type: array - type: object + items: + $ref: '#/definitions/repo.LocationOutCount' + type: array security: - Bearer: [] summary: Get All Locations @@ -1468,14 +1458,9 @@ paths: "200": description: OK schema: - allOf: - - $ref: '#/definitions/v1.Wrapped' - - properties: - items: - items: - $ref: '#/definitions/repo.TreeItem' - type: array - type: object + items: + $ref: '#/definitions/repo.TreeItem' + type: array security: - Bearer: [] summary: Get Locations Tree @@ -1489,14 +1474,9 @@ paths: "200": description: OK schema: - allOf: - - $ref: '#/definitions/v1.Wrapped' - - properties: - items: - items: - $ref: '#/definitions/repo.NotifierOut' - type: array - type: object + items: + $ref: '#/definitions/repo.NotifierOut' + type: array security: - Bearer: [] summary: Get Notifiers diff --git a/backend/internal/core/services/service_user.go b/backend/internal/core/services/service_user.go index bed3adb..b654029 100644 --- a/backend/internal/core/services/service_user.go +++ b/backend/internal/core/services/service_user.go @@ -61,6 +61,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration) switch data.GroupToken { case "": + log.Debug().Msg("creating new group") creatingGroup = true group, err = svc.repos.Groups.GroupCreate(ctx, "Home") if err != nil { @@ -68,6 +69,7 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration) return repo.UserOut{}, err } default: + log.Debug().Msg("joining existing group") token, err = svc.repos.Groups.InvitationGet(ctx, hasher.HashToken(data.GroupToken)) if err != nil { log.Err(err).Msg("Failed to get invitation token") @@ -94,14 +96,14 @@ func (svc *UserService) RegisterUser(ctx context.Context, data UserRegistration) // Create the default labels and locations for the group. if creatingGroup { for _, label := range defaultLabels() { - _, err := svc.repos.Labels.Create(ctx, group.ID, label) + _, err := svc.repos.Labels.Create(ctx, usr.GroupID, label) if err != nil { return repo.UserOut{}, err } } for _, location := range defaultLocations() { - _, err := svc.repos.Locations.Create(ctx, group.ID, location) + _, err := svc.repos.Locations.Create(ctx, usr.GroupID, location) if err != nil { return repo.UserOut{}, err } diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index 678130c..2b74071 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -234,11 +234,17 @@ func (r *GroupRepository) StatsGroup(ctx context.Context, GID uuid.UUID) (GroupS var stats GroupStatistics row := r.db.Sql().QueryRowContext(ctx, q, GID, GID, GID, GID, GID, GID) - err := row.Scan(&stats.TotalUsers, &stats.TotalItems, &stats.TotalLocations, &stats.TotalLabels, &stats.TotalItemPrice, &stats.TotalWithWarranty) + var maybeTotalItemPrice *float64 + var maybeTotalWithWarranty *int + + err := row.Scan(&stats.TotalUsers, &stats.TotalItems, &stats.TotalLocations, &stats.TotalLabels, &maybeTotalItemPrice, &maybeTotalWithWarranty) if err != nil { return GroupStatistics{}, err } + stats.TotalItemPrice = orDefault(maybeTotalItemPrice, 0) + stats.TotalWithWarranty = orDefault(maybeTotalWithWarranty, 0) + return stats, nil } diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 04c203b..103c3e9 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -52,7 +52,7 @@ type ( ImportRef string `json:"-"` ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"` Name string `json:"name" validate:"required,min=1,max=255"` - Description string `json:"description" validate:"required,min=1,max=1000"` + Description string `json:"description" validate:"max=1000"` AssetID AssetID `json:"-"` // Edges diff --git a/backend/internal/data/repo/repo_maintenance_entry.go b/backend/internal/data/repo/repo_maintenance_entry.go index 7ef282b..daf7887 100644 --- a/backend/internal/data/repo/repo_maintenance_entry.go +++ b/backend/internal/data/repo/repo_maintenance_entry.go @@ -2,6 +2,7 @@ package repo import ( "context" + "errors" "time" "github.com/google/uuid" @@ -18,15 +19,38 @@ import ( type MaintenanceEntryRepository struct { db *ent.Client } -type ( - MaintenanceEntryCreate struct { - CompletedDate types.Date `json:"completedDate"` - ScheduledDate types.Date `json:"scheduledDate"` - Name string `json:"name"` - Description string `json:"description"` - Cost float64 `json:"cost,string"` - } +type MaintenanceEntryCreate struct { + CompletedDate types.Date `json:"completedDate"` + ScheduledDate types.Date `json:"scheduledDate"` + Name string `json:"name" validate:"required"` + Description string `json:"description"` + Cost float64 `json:"cost,string"` +} + +func (mc MaintenanceEntryCreate) Validate() error { + if mc.CompletedDate.Time().IsZero() && mc.ScheduledDate.Time().IsZero() { + return errors.New("either completedDate or scheduledDate must be set") + } + return nil +} + +type MaintenanceEntryUpdate struct { + CompletedDate types.Date `json:"completedDate"` + ScheduledDate types.Date `json:"scheduledDate"` + Name string `json:"name"` + Description string `json:"description"` + Cost float64 `json:"cost,string"` +} + +func (mu MaintenanceEntryUpdate) Validate() error { + if mu.CompletedDate.Time().IsZero() && mu.ScheduledDate.Time().IsZero() { + return errors.New("either completedDate or scheduledDate must be set") + } + return nil +} + +type ( MaintenanceEntry struct { ID uuid.UUID `json:"id"` CompletedDate types.Date `json:"completedDate"` @@ -36,14 +60,6 @@ type ( Cost float64 `json:"cost,string"` } - MaintenanceEntryUpdate struct { - CompletedDate types.Date `json:"completedDate"` - ScheduledDate types.Date `json:"scheduledDate"` - Name string `json:"name"` - Description string `json:"description"` - Cost float64 `json:"cost,string"` - } - MaintenanceLog struct { ItemID uuid.UUID `json:"itemId"` CostAverage float64 `json:"costAverage"` diff --git a/backend/internal/web/adapters/decoders.go b/backend/internal/web/adapters/decoders.go index d1444ef..ef4bf6c 100644 --- a/backend/internal/web/adapters/decoders.go +++ b/backend/internal/web/adapters/decoders.go @@ -29,20 +29,31 @@ func DecodeQuery[T any](r *http.Request) (T, error) { return v, nil } +type Validator interface { + Validate() error +} + func DecodeBody[T any](r *http.Request) (T, error) { - var v T + var val T - err := server.Decode(r, &v) + err := server.Decode(r, &val) if err != nil { - return v, errors.Wrap(err, "body decoding error") + return val, errors.Wrap(err, "body decoding error") } - err = validate.Check(v) + err = validate.Check(val) if err != nil { - return v, errors.Wrap(err, "validation error") + return val, err } - return v, nil + if v, ok := any(val).(Validator); ok { + err = v.Validate() + if err != nil { + return val, errors.Wrap(err, "validation error") + } + } + + return val, nil } func RouteUUID(r *http.Request, key string) (uuid.UUID, error) { diff --git a/docs/docs/api/openapi-2.0.json b/docs/docs/api/openapi-2.0.json index c12bd42..0eaa5f4 100644 --- a/docs/docs/api/openapi-2.0.json +++ b/docs/docs/api/openapi-2.0.json @@ -937,22 +937,10 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LabelOut" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.LabelOut" + } } } } @@ -1109,22 +1097,10 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.LocationOutCount" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.LocationOutCount" + } } } } @@ -1189,22 +1165,10 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.TreeItem" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.TreeItem" + } } } } @@ -1329,22 +1293,10 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/v1.Wrapped" - }, - { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/repo.NotifierOut" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/repo.NotifierOut" + } } } } @@ -1909,14 +1861,12 @@ "repo.ItemCreate": { "type": "object", "required": [ - "description", "name" ], "properties": { "description": { "type": "string", - "maxLength": 1000, - "minLength": 1 + "maxLength": 1000 }, "labelIds": { "type": "array", @@ -2425,6 +2375,9 @@ }, "repo.MaintenanceEntryCreate": { "type": "object", + "required": [ + "name" + ], "properties": { "completedDate": { "description": "Sold", diff --git a/frontend/components/global/PageQRCode.vue b/frontend/components/global/PageQRCode.vue index 5c511d1..d6d0a11 100644 --- a/frontend/components/global/PageQRCode.vue +++ b/frontend/components/global/PageQRCode.vue @@ -15,12 +15,10 @@ diff --git a/frontend/composables/use-location-helpers.ts b/frontend/composables/use-location-helpers.ts index f9701de..5d4d245 100644 --- a/frontend/composables/use-location-helpers.ts +++ b/frontend/composables/use-location-helpers.ts @@ -14,6 +14,10 @@ export function flatTree(tree: TreeItem[]): Ref { // the display is a string of the tree hierarchy separated by breadcrumbs function flatten(items: TreeItem[], display: string) { + if (!items) { + return; + } + for (const item of items) { v.value.push({ id: item.id, @@ -40,5 +44,5 @@ export async function useFlatLocations(): Promise> { return ref([]); } - return flatTree(locations.data.items); + return flatTree(locations.data); } diff --git a/frontend/lib/api/classes/labels.ts b/frontend/lib/api/classes/labels.ts index 3a58bbe..61bcab7 100644 --- a/frontend/lib/api/classes/labels.ts +++ b/frontend/lib/api/classes/labels.ts @@ -1,10 +1,9 @@ import { BaseAPI, route } from "../base"; import { LabelCreate, LabelOut } from "../types/data-contracts"; -import { Results } from "../types/non-generated"; export class LabelsApi extends BaseAPI { getAll() { - return this.http.get>({ url: route("/labels") }); + return this.http.get({ url: route("/labels") }); } create(body: LabelCreate) { diff --git a/frontend/lib/api/classes/locations.ts b/frontend/lib/api/classes/locations.ts index a32b34d..acbbcdb 100644 --- a/frontend/lib/api/classes/locations.ts +++ b/frontend/lib/api/classes/locations.ts @@ -1,6 +1,5 @@ import { BaseAPI, route } from "../base"; import { LocationOutCount, LocationCreate, LocationOut, LocationUpdate, TreeItem } from "../types/data-contracts"; -import { Results } from "../types/non-generated"; export type LocationsQuery = { filterChildren: boolean; @@ -12,11 +11,11 @@ export type TreeQuery = { export class LocationsApi extends BaseAPI { getAll(q: LocationsQuery = { filterChildren: false }) { - return this.http.get>({ url: route("/locations", q) }); + return this.http.get({ url: route("/locations", q) }); } getTree(tq = { withItems: false }) { - return this.http.get>({ url: route("/locations/tree", tq) }); + return this.http.get({ url: route("/locations/tree", tq) }); } create(body: LocationCreate) { diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index b9be3ef..57b96c4 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -57,10 +57,7 @@ export interface ItemAttachmentUpdate { } export interface ItemCreate { - /** - * @minLength 1 - * @maxLength 1000 - */ + /** @maxLength 1000 */ description: string; labelIds: string[]; /** Edges */ diff --git a/frontend/lib/api/types/non-generated.ts b/frontend/lib/api/types/non-generated.ts index acb7e01..bc920de 100644 --- a/frontend/lib/api/types/non-generated.ts +++ b/frontend/lib/api/types/non-generated.ts @@ -10,10 +10,6 @@ export type Result = { item: T; }; -export type Results = { - items: T[]; -}; - export interface PaginationResult { items: T[]; page: number; diff --git a/frontend/pages/locations.vue b/frontend/pages/locations.vue index c360a02..ea84878 100644 --- a/frontend/pages/locations.vue +++ b/frontend/pages/locations.vue @@ -20,7 +20,7 @@ return []; } - return data.items; + return data; }); const locationTreeId = "locationTree"; diff --git a/frontend/stores/labels.ts b/frontend/stores/labels.ts index 05988d6..0cbbf75 100644 --- a/frontend/stores/labels.ts +++ b/frontend/stores/labels.ts @@ -19,7 +19,7 @@ export const useLabelStore = defineStore("labels", { console.error(result.error); } - this.allLabels = result.data.items; + this.allLabels = result.data; }); } return state.allLabels ?? []; @@ -32,7 +32,7 @@ export const useLabelStore = defineStore("labels", { return result; } - this.allLabels = result.data.items; + this.allLabels = result.data; return result; }, }, diff --git a/frontend/stores/locations.ts b/frontend/stores/locations.ts index c313040..2a5c83d 100644 --- a/frontend/stores/locations.ts +++ b/frontend/stores/locations.ts @@ -22,7 +22,7 @@ export const useLocationStore = defineStore("locations", { return; } - this.parents = result.data.items; + this.parents = result.data; }); } return state.parents ?? []; @@ -35,7 +35,7 @@ export const useLocationStore = defineStore("locations", { return; } - this.Locations = result.data.items; + this.Locations = result.data; }); } return state.Locations ?? []; @@ -48,7 +48,7 @@ export const useLocationStore = defineStore("locations", { return result; } - this.parents = result.data.items; + this.parents = result.data; return result; }, async refreshChildren(): ReturnType { @@ -57,7 +57,7 @@ export const useLocationStore = defineStore("locations", { return result; } - this.Locations = result.data.items; + this.Locations = result.data; return result; }, },