Skip to content

Commit

Permalink
fix: date and datetime regression (#282)
Browse files Browse the repository at this point in the history
* use custom types.Date implementation

* fix user registration bug

* remove sanity check

* fix datetime bug
  • Loading branch information
hay-kot committed Feb 15, 2023
1 parent 44f13f7 commit 607b06d
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 52 deletions.
3 changes: 2 additions & 1 deletion backend/app/api/handlers/v1/v1_ctrl_user.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v1

import (
"fmt"
"net/http"

"github.com/google/uuid"
Expand Down Expand Up @@ -28,7 +29,7 @@ func (ctrl *V1Controller) HandleUserRegistration() server.HandlerFunc {
}

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)
Expand Down
1 change: 1 addition & 0 deletions backend/app/api/static/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1983,6 +1983,7 @@ const docTemplate = `{
"type": "string"
},
"warrantyExpires": {
"description": "Sold",
"type": "string"
}
}
Expand Down
1 change: 1 addition & 0 deletions backend/app/api/static/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1975,6 +1975,7 @@
"type": "string"
},
"warrantyExpires": {
"description": "Sold",
"type": "string"
}
}
Expand Down
1 change: 1 addition & 0 deletions backend/app/api/static/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ definitions:
warrantyDetails:
type: string
warrantyExpires:
description: Sold
type: string
type: object
repo.LabelCreate:
Expand Down
18 changes: 5 additions & 13 deletions backend/internal/core/services/service_items_csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"io"
"strconv"
"strings"
"time"

"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
)

func determineSeparator(data []byte) (rune, error) {
Expand Down Expand Up @@ -62,15 +62,6 @@ func parseFloat(s string) float64 {
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 {
switch strings.ToLower(s) {
case "true", "yes", "1":
Expand All @@ -92,6 +83,7 @@ type csvRow struct {
}

func newCsvRow(row []string) csvRow {

return csvRow{
Location: row[1],
LabelStr: row[2],
Expand All @@ -109,13 +101,13 @@ func newCsvRow(row []string) csvRow {
Manufacturer: row[9],
Notes: row[10],
PurchaseFrom: row[11],
PurchaseTime: parseDate(row[13]),
PurchaseTime: types.DateFromString(row[13]),
LifetimeWarranty: parseBool(row[14]),
WarrantyExpires: parseDate(row[15]),
WarrantyExpires: types.DateFromString(row[15]),
WarrantyDetails: row[16],
SoldTo: row[17],
SoldPrice: parseFloat(row[18]),
SoldTime: parseDate(row[19]),
SoldTime: types.DateFromString(row[19]),
SoldNotes: row[20],
},
}
Expand Down
6 changes: 3 additions & 3 deletions backend/internal/core/services/service_items_csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ func Test_CorrectDateParsing(t *testing.T) {
entity := newCsvRow(record)
expected := expected[i-1]

assert.Equal(t, expected, entity.Item.PurchaseTime, 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.SoldTime, 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.Time(), fmt.Sprintf("Failed on row %d", i))
assert.Equal(t, expected, entity.Item.SoldTime.Time(), fmt.Sprintf("Failed on row %d", i))
}
}

Expand Down
51 changes: 26 additions & 25 deletions backend/internal/data/repo/repo_items.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"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/predicate"
"github.com/hay-kot/homebox/backend/internal/data/types"
)

type ItemsRepository struct {
Expand Down Expand Up @@ -78,20 +79,20 @@ type (
Manufacturer string `json:"manufacturer"`

// Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"`
LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires types.Date `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"`

// Purchase
PurchaseTime time.Time `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`
PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`

// Sold
SoldTime time.Time `json:"soldTime"`
SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"`
SoldTime types.Date `json:"soldTime"`
SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"`

// Extras
Notes string `json:"notes"`
Expand Down Expand Up @@ -126,19 +127,19 @@ type (
Manufacturer string `json:"manufacturer"`

// Warranty
LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires time.Time `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"`
LifetimeWarranty bool `json:"lifetimeWarranty"`
WarrantyExpires types.Date `json:"warrantyExpires"`
WarrantyDetails string `json:"warrantyDetails"`

// Purchase
PurchaseTime time.Time `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchaseTime types.Date `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`

// Sold
SoldTime time.Time `json:"soldTime"`
SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"`
SoldTime types.Date `json:"soldTime"`
SoldTo string `json:"soldTo"`
SoldPrice float64 `json:"soldPrice,string"`
SoldNotes string `json:"soldNotes"`

// Extras
Notes string `json:"notes"`
Expand Down Expand Up @@ -232,7 +233,7 @@ func mapItemOut(item *ent.Item) ItemOut {
AssetID: AssetID(item.AssetID),
ItemSummary: mapItemSummary(item),
LifetimeWarranty: item.LifetimeWarranty,
WarrantyExpires: item.WarrantyExpires,
WarrantyExpires: types.DateFromTime(item.WarrantyExpires),
WarrantyDetails: item.WarrantyDetails,

// Identification
Expand All @@ -241,11 +242,11 @@ func mapItemOut(item *ent.Item) ItemOut {
Manufacturer: item.Manufacturer,

// Purchase
PurchaseTime: item.PurchaseTime,
PurchaseTime: types.DateFromTime(item.PurchaseTime),
PurchaseFrom: item.PurchaseFrom,

// Sold
SoldTime: item.SoldTime,
SoldTime: types.DateFromTime(item.SoldTime),
SoldTo: item.SoldTo,
SoldPrice: item.SoldPrice,
SoldNotes: item.SoldNotes,
Expand Down Expand Up @@ -526,17 +527,17 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data
SetModelNumber(data.ModelNumber).
SetManufacturer(data.Manufacturer).
SetArchived(data.Archived).
SetPurchaseTime(data.PurchaseTime).
SetPurchaseTime(data.PurchaseTime.Time()).
SetPurchaseFrom(data.PurchaseFrom).
SetPurchasePrice(data.PurchasePrice).
SetSoldTime(data.SoldTime).
SetSoldTime(data.SoldTime.Time()).
SetSoldTo(data.SoldTo).
SetSoldPrice(data.SoldPrice).
SetSoldNotes(data.SoldNotes).
SetNotes(data.Notes).
SetLifetimeWarranty(data.LifetimeWarranty).
SetInsured(data.Insured).
SetWarrantyExpires(data.WarrantyExpires).
SetWarrantyExpires(data.WarrantyExpires.Time()).
SetWarrantyDetails(data.WarrantyDetails).
SetQuantity(data.Quantity).
SetAssetID(int(data.AssetID))
Expand Down
7 changes: 4 additions & 3 deletions backend/internal/data/repo/repo_items_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -237,15 +238,15 @@ func TestItemsRepository_Update(t *testing.T) {
LabelIDs: nil,
ModelNumber: fk.Str(10),
Manufacturer: fk.Str(10),
PurchaseTime: time.Now(),
PurchaseTime: types.DateFromTime(time.Now()),
PurchaseFrom: fk.Str(10),
PurchasePrice: 300.99,
SoldTime: time.Now(),
SoldTime: types.DateFromTime(time.Now()),
SoldTo: fk.Str(10),
SoldPrice: 300.99,
SoldNotes: fk.Str(10),
Notes: fk.Str(10),
WarrantyExpires: time.Now(),
WarrantyExpires: types.DateFromTime(time.Now()),
WarrantyDetails: fk.Str(10),
LifetimeWarranty: true,
}
Expand Down
88 changes: 88 additions & 0 deletions backend/internal/data/types/date.go
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 31 additions & 7 deletions frontend/lib/api/base/base-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,45 @@ export function hasKey(obj: object, key: string): obj is Required<BaseApiType> {
export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
const result = { ...obj };
[...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 (result[key] === ZERO_DATE) {
const value = result[key] as string;

if (value === undefined || value === "" || value.startsWith(ZERO_DATE)) {
const dt = new Date();
dt.setFullYear(1);

result[key] = dt;
return;
}

// transform string to ensure dates are parsed as UTC dates instead of
// localized time stamps
const asStr = result[key] as string;
const cleaned = asStr.replaceAll("-", "/").split("T")[0];
result[key] = new Date(cleaned);
// Possible Formats
// Date Only: YYYY-MM-DD
// Timestamp: 0001-01-01T00:00:00Z

// 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;
}
});

Expand Down

0 comments on commit 607b06d

Please sign in to comment.