fix: date and datetime regression (#282)

* use custom types.Date implementation

* fix user registration bug

* remove sanity check

* fix datetime bug
This commit is contained in:
Hayden 2023-02-15 08:40:35 -09:00 committed by GitHub
parent 44f13f751a
commit 607b06d2f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 162 additions and 52 deletions

View file

@ -1,6 +1,7 @@
package v1 package v1
import ( import (
"fmt"
"net/http" "net/http"
"github.com/google/uuid" "github.com/google/uuid"
@ -28,7 +29,7 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
} }
if !ctrl.allowRegistration && regData.GroupToken == "" { if !ctrl.allowRegistration && regData.GroupToken == "" {
return validate.NewRequestError(nil, http.StatusForbidden) return validate.NewRequestError(fmt.Errorf("user registration disabled"), http.StatusForbidden)
} }
_, err := ctrl.svc.User.RegisterUser(r.Context(), regData) _, err := ctrl.svc.User.RegisterUser(r.Context(), regData)

View file

@ -1983,6 +1983,7 @@ const docTemplate = `{
"type": "string" "type": "string"
}, },
"warrantyExpires": { "warrantyExpires": {
"description": "Sold",
"type": "string" "type": "string"
} }
} }

View file

@ -1975,6 +1975,7 @@
"type": "string" "type": "string"
}, },
"warrantyExpires": { "warrantyExpires": {
"description": "Sold",
"type": "string" "type": "string"
} }
} }

View file

@ -275,6 +275,7 @@ definitions:
warrantyDetails: warrantyDetails:
type: string type: string
warrantyExpires: warrantyExpires:
description: Sold
type: string type: string
type: object type: object
repo.LabelCreate: repo.LabelCreate:

View file

@ -7,9 +7,9 @@ import (
"io" "io"
"strconv" "strconv"
"strings" "strings"
"time"
"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/data/types"
) )
func determineSeparator(data []byte) (rune, error) { func determineSeparator(data []byte) (rune, error) {
@ -62,15 +62,6 @@ func parseFloat(s string) float64 {
return f return f
} }
func parseDate(s string) time.Time {
if s == "" {
return time.Time{}
}
p, _ := time.Parse("01/02/2006", s)
return p
}
func parseBool(s string) bool { func parseBool(s string) bool {
switch strings.ToLower(s) { switch strings.ToLower(s) {
case "true", "yes", "1": case "true", "yes", "1":
@ -92,6 +83,7 @@ type csvRow struct {
} }
func newCsvRow(row []string) csvRow { func newCsvRow(row []string) csvRow {
return csvRow{ return csvRow{
Location: row[1], Location: row[1],
LabelStr: row[2], LabelStr: row[2],
@ -109,13 +101,13 @@ func newCsvRow(row []string) csvRow {
Manufacturer: row[9], Manufacturer: row[9],
Notes: row[10], Notes: row[10],
PurchaseFrom: row[11], PurchaseFrom: row[11],
PurchaseTime: parseDate(row[13]), PurchaseTime: types.DateFromString(row[13]),
LifetimeWarranty: parseBool(row[14]), LifetimeWarranty: parseBool(row[14]),
WarrantyExpires: parseDate(row[15]), WarrantyExpires: types.DateFromString(row[15]),
WarrantyDetails: row[16], WarrantyDetails: row[16],
SoldTo: row[17], SoldTo: row[17],
SoldPrice: parseFloat(row[18]), SoldPrice: parseFloat(row[18]),
SoldTime: parseDate(row[19]), SoldTime: types.DateFromString(row[19]),
SoldNotes: row[20], SoldNotes: row[20],
}, },
} }

View file

@ -50,9 +50,9 @@ func Test_CorrectDateParsing(t *testing.T) {
entity := newCsvRow(record) entity := newCsvRow(record)
expected := expected[i-1] expected := expected[i-1]
assert.Equal(t, expected, entity.Item.PurchaseTime, fmt.Sprintf("Failed on row %d", i)) assert.Equal(t, expected, entity.Item.PurchaseTime.Time(), fmt.Sprintf("Failed on row %d", i))
assert.Equal(t, expected, entity.Item.WarrantyExpires, 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, fmt.Sprintf("Failed on row %d", i)) assert.Equal(t, expected, entity.Item.SoldTime.Time(), fmt.Sprintf("Failed on row %d", i))
} }
} }

View file

@ -13,6 +13,7 @@ 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/predicate" "github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
"github.com/hay-kot/homebox/backend/internal/data/types"
) )
type ItemsRepository struct { type ItemsRepository struct {
@ -78,20 +79,20 @@ type (
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`
// Warranty // Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"` LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"` WarrantyExpires types.Date `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"` WarrantyDetails string `json:"warrantyDetails"`
// Purchase // Purchase
PurchaseTime time.Time `json:"purchaseTime"` PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"` PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"` PurchasePrice float64 `json:"purchasePrice,string"`
// Sold // Sold
SoldTime time.Time `json:"soldTime"` SoldTime types.Date `json:"soldTime"`
SoldTo string `json:"soldTo"` SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"` SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"` SoldNotes string `json:"soldNotes"`
// Extras // Extras
Notes string `json:"notes"` Notes string `json:"notes"`
@ -126,19 +127,19 @@ type (
Manufacturer string `json:"manufacturer"` Manufacturer string `json:"manufacturer"`
// Warranty // Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"` LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"` WarrantyExpires types.Date `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"` WarrantyDetails string `json:"warrantyDetails"`
// Purchase // Purchase
PurchaseTime time.Time `json:"purchaseTime"` PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"` PurchaseFrom string `json:"purchaseFrom"`
// Sold // Sold
SoldTime time.Time `json:"soldTime"` SoldTime types.Date `json:"soldTime"`
SoldTo string `json:"soldTo"` SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"` SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"` SoldNotes string `json:"soldNotes"`
// Extras // Extras
Notes string `json:"notes"` Notes string `json:"notes"`
@ -232,7 +233,7 @@ func mapItemOut(item *ent.Item) ItemOut {
AssetID: AssetID(item.AssetID), AssetID: AssetID(item.AssetID),
ItemSummary: mapItemSummary(item), ItemSummary: mapItemSummary(item),
LifetimeWarranty: item.LifetimeWarranty, LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: item.WarrantyExpires, WarrantyExpires: types.DateFromTime(item.WarrantyExpires),
WarrantyDetails: item.WarrantyDetails, WarrantyDetails: item.WarrantyDetails,
// Identification // Identification
@ -241,11 +242,11 @@ func mapItemOut(item *ent.Item) ItemOut {
Manufacturer: item.Manufacturer, Manufacturer: item.Manufacturer,
// Purchase // Purchase
PurchaseTime: item.PurchaseTime, PurchaseTime: types.DateFromTime(item.PurchaseTime),
PurchaseFrom: item.PurchaseFrom, PurchaseFrom: item.PurchaseFrom,
// Sold // Sold
SoldTime: item.SoldTime, SoldTime: types.DateFromTime(item.SoldTime),
SoldTo: item.SoldTo, SoldTo: item.SoldTo,
SoldPrice: item.SoldPrice, SoldPrice: item.SoldPrice,
SoldNotes: item.SoldNotes, SoldNotes: item.SoldNotes,
@ -526,17 +527,17 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data
SetModelNumber(data.ModelNumber). SetModelNumber(data.ModelNumber).
SetManufacturer(data.Manufacturer). SetManufacturer(data.Manufacturer).
SetArchived(data.Archived). SetArchived(data.Archived).
SetPurchaseTime(data.PurchaseTime). SetPurchaseTime(data.PurchaseTime.Time()).
SetPurchaseFrom(data.PurchaseFrom). SetPurchaseFrom(data.PurchaseFrom).
SetPurchasePrice(data.PurchasePrice). SetPurchasePrice(data.PurchasePrice).
SetSoldTime(data.SoldTime). SetSoldTime(data.SoldTime.Time()).
SetSoldTo(data.SoldTo). SetSoldTo(data.SoldTo).
SetSoldPrice(data.SoldPrice). SetSoldPrice(data.SoldPrice).
SetSoldNotes(data.SoldNotes). SetSoldNotes(data.SoldNotes).
SetNotes(data.Notes). SetNotes(data.Notes).
SetLifetimeWarranty(data.LifetimeWarranty). SetLifetimeWarranty(data.LifetimeWarranty).
SetInsured(data.Insured). SetInsured(data.Insured).
SetWarrantyExpires(data.WarrantyExpires). SetWarrantyExpires(data.WarrantyExpires.Time()).
SetWarrantyDetails(data.WarrantyDetails). SetWarrantyDetails(data.WarrantyDetails).
SetQuantity(data.Quantity). SetQuantity(data.Quantity).
SetAssetID(int(data.AssetID)) SetAssetID(int(data.AssetID))

View file

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -237,15 +238,15 @@ func TestItemsRepository_Update(t *testing.T) {
LabelIDs: nil, LabelIDs: nil,
ModelNumber: fk.Str(10), ModelNumber: fk.Str(10),
Manufacturer: fk.Str(10), Manufacturer: fk.Str(10),
PurchaseTime: time.Now(), PurchaseTime: types.DateFromTime(time.Now()),
PurchaseFrom: fk.Str(10), PurchaseFrom: fk.Str(10),
PurchasePrice: 300.99, PurchasePrice: 300.99,
SoldTime: time.Now(), SoldTime: types.DateFromTime(time.Now()),
SoldTo: fk.Str(10), SoldTo: fk.Str(10),
SoldPrice: 300.99, SoldPrice: 300.99,
SoldNotes: fk.Str(10), SoldNotes: fk.Str(10),
Notes: fk.Str(10), Notes: fk.Str(10),
WarrantyExpires: time.Now(), WarrantyExpires: types.DateFromTime(time.Now()),
WarrantyDetails: fk.Str(10), WarrantyDetails: fk.Str(10),
LifetimeWarranty: true, LifetimeWarranty: true,
} }

View file

@ -0,0 +1,88 @@
package types
import "time"
// Date is a custom type that implements the MarshalJSON interface
// that applies date only formatting to the time.Time fields in order
// to avoid common time and timezone pitfalls when working with Times.
//
// Examples:
//
// "2019-01-01" -> time.Time{2019-01-01 00:00:00 +0000 UTC}
// "2019-01-01T21:10:30Z" -> time.Time{2019-01-01 00:00:00 +0000 UTC}
// "2019-01-01T21:10:30+01:00" -> time.Time{2019-01-01 00:00:00 +0000 UTC}
type Date time.Time
func (d Date) Time() time.Time {
return time.Time(d)
}
// DateFromTime returns a Date type from a time.Time type by stripping
// the time and timezone information.
func DateFromTime(t time.Time) Date {
dateOnlyTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
return Date(dateOnlyTime)
}
// DateFromString returns a Date type from a string by parsing the
// string into a time.Time type and then stripping the time and
// timezone information.
//
// Errors are ignored and an empty Date is returned.
func DateFromString(s string) Date {
if s == "" {
return Date{}
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
// TODO: Remove - used by legacy importer
t, err = time.Parse("01/02/2006", s)
if err != nil {
return Date{}
}
}
return DateFromTime(t)
}
func (d Date) String() string {
if time.Time(d).IsZero() {
return ""
}
return time.Time(d).Format("2006-01-02")
}
func (d Date) MarshalJSON() ([]byte, error) {
if time.Time(d).IsZero() {
return []byte(`""`), nil
}
return []byte(`"` + d.String() + `"`), nil
}
func (d *Date) UnmarshalJSON(data []byte) error {
str := string(data)
if str == `""` {
*d = Date{}
return nil
}
// Try YYYY-MM-DD format
var t time.Time
t, err := time.Parse("2006-01-02", str)
if err != nil {
// Try default interface
err = t.UnmarshalJSON(data)
if err != nil {
return err
}
}
// strip the time and timezone information
*d = DateFromTime(t)
return nil
}

View file

@ -16,9 +16,11 @@ export function hasKey(obj: object, key: string): obj is Required<BaseApiType> {
export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T { export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
const result = { ...obj }; const result = { ...obj };
[...keys, "createdAt", "updatedAt"].forEach(key => { [...keys, "createdAt", "updatedAt"].forEach(key => {
// @ts-ignore - TS doesn't know that we're checking for the key above // @ts-expect-error - TS doesn't know that we're checking for the key above
if (hasKey(result, key)) { if (hasKey(result, key)) {
if (result[key] === ZERO_DATE) { const value = result[key] as string;
if (value === undefined || value === "" || value.startsWith(ZERO_DATE)) {
const dt = new Date(); const dt = new Date();
dt.setFullYear(1); dt.setFullYear(1);
@ -26,11 +28,33 @@ export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
return; return;
} }
// transform string to ensure dates are parsed as UTC dates instead of // Possible Formats
// localized time stamps // Date Only: YYYY-MM-DD
const asStr = result[key] as string; // Timestamp: 0001-01-01T00:00:00Z
const cleaned = asStr.replaceAll("-", "/").split("T")[0];
result[key] = new Date(cleaned); // Parse timestamps with default date
if (value.includes("T")) {
result[key] = new Date(value);
return;
}
// Parse dates with default time
const split = value.split("-");
if (split.length !== 3) {
console.log(`Invalid date format: ${value}`);
throw new Error(`Invalid date format: ${value}`);
}
const [year, month, day] = split;
const dt = new Date();
dt.setFullYear(parseInt(year, 10));
dt.setMonth(parseInt(month, 10) - 1);
dt.setDate(parseInt(day, 10));
result[key] = dt;
} }
}); });