conditionally filter parent locations

This commit is contained in:
Hayden 2022-11-02 11:29:17 -08:00
parent fbcbde836a
commit 8a71a51c43
No known key found for this signature in database
GPG key ID: 17CF79474E257545
18 changed files with 135 additions and 67 deletions

View file

@ -0,0 +1,36 @@
package v1
import (
"net/url"
"strconv"
"github.com/google/uuid"
)
func queryUUIDList(params url.Values, key string) []uuid.UUID {
var ids []uuid.UUID
for _, id := range params[key] {
uid, err := uuid.Parse(id)
if err != nil {
continue
}
ids = append(ids, uid)
}
return ids
}
func queryIntOrNegativeOne(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
return -1
}
return i
}
func queryBool(s string) bool {
b, err := strconv.ParseBool(s)
if err != nil {
return false
}
return b
}

View file

@ -3,10 +3,7 @@ package v1
import ( import (
"encoding/csv" "encoding/csv"
"net/http" "net/http"
"net/url"
"strconv"
"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"
@ -27,44 +24,17 @@ import (
// @Router /v1/items [GET] // @Router /v1/items [GET]
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc { func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
uuidList := func(params url.Values, key string) []uuid.UUID {
var ids []uuid.UUID
for _, id := range params[key] {
uid, err := uuid.Parse(id)
if err != nil {
continue
}
ids = append(ids, uid)
}
return ids
}
intOrNegativeOne := func(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
return -1
}
return i
}
getBool := func(s string) bool {
b, err := strconv.ParseBool(s)
if err != nil {
return false
}
return b
}
extractQuery := func(r *http.Request) repo.ItemQuery { extractQuery := func(r *http.Request) repo.ItemQuery {
params := r.URL.Query() params := r.URL.Query()
return repo.ItemQuery{ return repo.ItemQuery{
Page: intOrNegativeOne(params.Get("page")), Page: queryIntOrNegativeOne(params.Get("page")),
PageSize: intOrNegativeOne(params.Get("perPage")), PageSize: queryIntOrNegativeOne(params.Get("perPage")),
Search: params.Get("q"), Search: params.Get("q"),
LocationIDs: uuidList(params, "locations"), LocationIDs: queryUUIDList(params, "locations"),
LabelIDs: uuidList(params, "labels"), LabelIDs: queryUUIDList(params, "labels"),
IncludeArchived: getBool(params.Get("includeArchived")), IncludeArchived: queryBool(params.Get("includeArchived")),
} }
} }

View file

@ -15,13 +15,21 @@ import (
// @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"
// @Success 200 {object} server.Results{items=[]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() server.HandlerFunc { func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error { return func(w http.ResponseWriter, r *http.Request) error {
user := services.UseUserCtx(r.Context()) user := services.UseUserCtx(r.Context())
locations, err := ctrl.repo.Locations.GetAll(r.Context(), user.GroupID)
q := r.URL.Query()
filter := repo.LocationQuery{
FilterChildren: queryBool(q.Get("filterChildren")),
}
locations, err := ctrl.repo.Locations.GetAll(r.Context(), user.GroupID, filter)
if err != nil { if err != nil {
log.Err(err).Msg("failed to get locations") log.Err(err).Msg("failed to get locations")
return validate.NewRequestError(err, http.StatusInternalServerError) return validate.NewRequestError(err, http.StatusInternalServerError)

View file

@ -756,6 +756,14 @@ const docTemplate = `{
"Locations" "Locations"
], ],
"summary": "Get All Locations", "summary": "Get All Locations",
"parameters": [
{
"type": "boolean",
"description": "Filter locations with parents",
"name": "filterChildren",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",

View file

@ -748,6 +748,14 @@
"Locations" "Locations"
], ],
"summary": "Get All Locations", "summary": "Get All Locations",
"parameters": [
{
"type": "boolean",
"description": "Filter locations with parents",
"name": "filterChildren",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",

View file

@ -960,6 +960,11 @@ paths:
- Labels - Labels
/v1/locations: /v1/locations:
get: get:
parameters:
- description: Filter locations with parents
in: query
name: filterChildren
type: boolean
produces: produces:
- application/json - application/json
responses: responses:

View file

@ -23,7 +23,7 @@ type ItemService struct {
at attachmentTokens at attachmentTokens
} }
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) (int, error) { func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]string) (int, error) {
loaded := []csvRow{} loaded := []csvRow{}
// Skip first row // Skip first row
@ -66,7 +66,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
// Bootstrap the locations and labels so we can reuse the created IDs for the items // Bootstrap the locations and labels so we can reuse the created IDs for the items
locations := map[string]uuid.UUID{} locations := map[string]uuid.UUID{}
existingLocation, err := svc.repo.Locations.GetAll(ctx, gid) existingLocation, err := svc.repo.Locations.GetAll(ctx, GID, repo.LocationQuery{})
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -75,7 +75,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
} }
labels := map[string]uuid.UUID{} labels := map[string]uuid.UUID{}
existingLabels, err := svc.repo.Labels.GetAll(ctx, gid) existingLabels, err := svc.repo.Labels.GetAll(ctx, GID)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -87,7 +87,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
// Locations // Locations
if _, exists := locations[row.Location]; !exists { if _, exists := locations[row.Location]; !exists {
result, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{ result, err := svc.repo.Locations.Create(ctx, GID, repo.LocationCreate{
Name: row.Location, Name: row.Location,
Description: "", Description: "",
}) })
@ -103,7 +103,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
if _, exists := labels[label]; exists { if _, exists := labels[label]; exists {
continue continue
} }
result, err := svc.repo.Labels.Create(ctx, gid, repo.LabelCreate{ result, err := svc.repo.Labels.Create(ctx, GID, repo.LabelCreate{
Name: label, Name: label,
Description: "", Description: "",
}) })
@ -119,7 +119,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
for _, row := range loaded { for _, row := range loaded {
// Check Import Ref // Check Import Ref
if row.Item.ImportRef != "" { if row.Item.ImportRef != "" {
exists, err := svc.repo.Items.CheckRef(ctx, gid, row.Item.ImportRef) exists, err := svc.repo.Items.CheckRef(ctx, GID, row.Item.ImportRef)
if exists { if exists {
continue continue
} }
@ -139,7 +139,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
Str("location", row.Location). Str("location", row.Location).
Msgf("Creating Item: %s", row.Item.Name) Msgf("Creating Item: %s", row.Item.Name)
result, err := svc.repo.Items.Create(ctx, gid, repo.ItemCreate{ result, err := svc.repo.Items.Create(ctx, GID, repo.ItemCreate{
ImportRef: row.Item.ImportRef, ImportRef: row.Item.ImportRef,
Name: row.Item.Name, Name: row.Item.Name,
Description: row.Item.Description, Description: row.Item.Description,
@ -152,7 +152,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
} }
// Update the item with the rest of the data // Update the item with the rest of the data
_, err = svc.repo.Items.UpdateByGroup(ctx, gid, repo.ItemUpdate{ _, err = svc.repo.Items.UpdateByGroup(ctx, GID, repo.ItemUpdate{
// Edges // Edges
LocationID: locationID, LocationID: locationID,
LabelIDs: labelIDs, LabelIDs: labelIDs,

View file

@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -38,7 +39,7 @@ func TestItemService_CsvImport(t *testing.T) {
dataCsv = append(dataCsv, newCsvRow(item)) dataCsv = append(dataCsv, newCsvRow(item))
} }
allLocation, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID) allLocation, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID, repo.LocationQuery{})
assert.NoError(t, err) assert.NoError(t, err)
locNames := []string{} locNames := []string{}
for _, loc := range allLocation { for _, loc := range allLocation {

View file

@ -2,6 +2,7 @@ package repo
import ( import (
"context" "context"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -90,8 +91,12 @@ func mapLocationOut(location *ent.Location) LocationOut {
} }
} }
type LocationQuery struct {
FilterChildren bool `json:"filterChildren"`
}
// GetALlWithCount returns all locations with item count field populated // GetALlWithCount returns all locations with item count field populated
func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]LocationOutCount, error) { func (r *LocationRepository) GetAll(ctx context.Context, GID uuid.UUID, filter LocationQuery) ([]LocationOutCount, error) {
query := `--sql query := `--sql
SELECT SELECT
id, id,
@ -111,13 +116,18 @@ func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]L
FROM FROM
locations locations
WHERE WHERE
locations.group_locations = ? locations.group_locations = ? {{ FILTER_CHILDREN }}
AND locations.location_children IS NULL
ORDER BY ORDER BY
locations.name ASC locations.name ASC
` `
rows, err := r.db.Sql().QueryContext(ctx, query, groupId) if filter.FilterChildren {
query = strings.Replace(query, "{{ FILTER_CHILDREN }}", "AND locations.location_children IS NULL", 1)
} else {
query = strings.Replace(query, "{{ FILTER_CHILDREN }}", "", 1)
}
rows, err := r.db.Sql().QueryContext(ctx, query, GID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -43,7 +43,7 @@ func TestLocationRepositoryGetAllWithCount(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
results, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID) results, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID, LocationQuery{})
assert.NoError(t, err) assert.NoError(t, err)
for _, loc := range results { for _, loc := range results {

View file

@ -41,7 +41,7 @@
const toast = useNotifier(); const toast = useNotifier();
const locationsStore = useLocationStore(); const locationsStore = useLocationStore();
const locations = computed(() => locationsStore.locations); const locations = computed(() => locationsStore.allLocations);
const labelStore = useLabelStore(); const labelStore = useLabelStore();
const labels = computed(() => labelStore.labels); const labels = computed(() => labelStore.labels);

View file

@ -32,7 +32,8 @@
const rmLocationStoreObserver = defineObserver("locationStore", { const rmLocationStoreObserver = defineObserver("locationStore", {
handler: r => { handler: r => {
if (r.status === 201 || r.url.match(reLocation)) { if (r.status === 201 || r.url.match(reLocation)) {
locationStore.refresh(); locationStore.refreshChildren();
locationStore.refreshParents();
} }
console.debug("locationStore handler called by observer"); console.debug("locationStore handler called by observer");
}, },
@ -43,7 +44,8 @@
EventTypes.ClearStores, EventTypes.ClearStores,
() => { () => {
labelStore.refresh(); labelStore.refresh();
locationStore.refresh(); locationStore.refreshChildren();
locationStore.refreshParents();
}, },
"stores" "stores"
); );

View file

@ -2,9 +2,13 @@ import { BaseAPI, route } from "../base";
import { LocationOutCount, LocationCreate, LocationOut, LocationUpdate } from "../types/data-contracts"; import { LocationOutCount, LocationCreate, LocationOut, LocationUpdate } from "../types/data-contracts";
import { Results } from "../types/non-generated"; import { Results } from "../types/non-generated";
export type LocationsQuery = {
filterChildren: boolean;
};
export class LocationsApi extends BaseAPI { export class LocationsApi extends BaseAPI {
getAll() { getAll(q: LocationsQuery = { filterChildren: false }) {
return this.http.get<Results<LocationOutCount>>({ url: route("/locations") }); return this.http.get<Results<LocationOutCount>>({ url: route("/locations", q) });
} }
create(body: LocationCreate) { create(body: LocationCreate) {

View file

@ -16,7 +16,7 @@
const auth = useAuthStore(); const auth = useAuthStore();
const locationStore = useLocationStore(); const locationStore = useLocationStore();
const locations = computed(() => locationStore.locations); const locations = computed(() => locationStore.parentLocations);
const labelsStore = useLabelStore(); const labelsStore = useLabelStore();
const labels = computed(() => labelsStore.labels); const labels = computed(() => labelsStore.labels);

View file

@ -18,7 +18,7 @@
const itemId = computed<string>(() => route.params.id as string); const itemId = computed<string>(() => route.params.id as string);
const locationStore = useLocationStore(); const locationStore = useLocationStore();
const locations = computed(() => locationStore.locations); const locations = computed(() => locationStore.allLocations);
const labelStore = useLabelStore(); const labelStore = useLabelStore();
const labels = computed(() => labelStore.labels); const labels = computed(() => labelStore.labels);

View file

@ -82,7 +82,7 @@
}); });
const locationsStore = useLocationStore(); const locationsStore = useLocationStore();
const locations = computed(() => locationsStore.locations); const locations = computed(() => locationsStore.allLocations);
const labelStore = useLabelStore(); const labelStore = useLabelStore();
const labels = computed(() => labelStore.labels); const labels = computed(() => labelStore.labels);

View file

@ -117,7 +117,7 @@
} }
const locationStore = useLocationStore(); const locationStore = useLocationStore();
const locations = computed(() => locationStore.locations); const locations = computed(() => locationStore.allLocations);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const parent = ref<LocationSummary | any>({}); const parent = ref<LocationSummary | any>({});

View file

@ -3,7 +3,8 @@ import { LocationOutCount } from "~~/lib/api/types/data-contracts";
export const useLocationStore = defineStore("locations", { export const useLocationStore = defineStore("locations", {
state: () => ({ state: () => ({
allLocations: null as LocationOutCount[] | null, parents: null as LocationOutCount[] | null,
Locations: null as LocationOutCount[] | null,
client: useUserApi(), client: useUserApi(),
}), }),
getters: { getters: {
@ -12,21 +13,36 @@ export const useLocationStore = defineStore("locations", {
* synched with the server by intercepting the API calls and updating on the * synched with the server by intercepting the API calls and updating on the
* response * response
*/ */
locations(state): LocationOutCount[] { parentLocations(state): LocationOutCount[] {
if (state.allLocations === null) { if (state.parents === null) {
Promise.resolve(this.refresh()); Promise.resolve(this.refreshParents());
} }
return state.allLocations; return state.parents;
},
allLocations(state): LocationOutCount[] {
if (state.Locations === null) {
Promise.resolve(this.refreshChildren());
}
return state.Locations;
}, },
}, },
actions: { actions: {
async refresh(): Promise<LocationOutCount[]> { async refreshParents(): Promise<LocationOutCount[]> {
const result = await this.client.locations.getAll(); const result = await this.client.locations.getAll({ filterChildren: true });
if (result.error) { if (result.error) {
return result; return result;
} }
this.allLocations = result.data.items; this.parents = result.data.items;
return result;
},
async refreshChildren(): Promise<LocationOutCount[]> {
const result = await this.client.locations.getAll({ filterChildren: false });
if (result.error) {
return result;
}
this.Locations = result.data.items;
return result; return result;
}, },
}, },