Skip to content

Commit b6601e5

Browse files
committed
Fixed parsing of timestamps when flattening Firebase documents (timestamps are left as strings within nested arrays and maps)
1 parent 688aeb0 commit b6601e5

File tree

3 files changed

+67
-32
lines changed

3 files changed

+67
-32
lines changed

internal/testutil/test_data.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ var TestFirebaseDocs = []map[string]any{
5959
var TestFirebaseDocFields = []map[string]any{
6060
{
6161
"timeData": map[string]any{
62-
"timestampValue": "2025-04-14T01:02:03Z",
62+
"timestampValue": testTimestamp,
6363
},
6464
},
6565
{
@@ -111,7 +111,7 @@ var TestFirebaseDocFields = []map[string]any{
111111
"mapValue": map[string]any{
112112
"fields": map[string]any{
113113
"timeData": map[string]any{
114-
"timestampValue": "2025-04-14T01:02:03Z",
114+
"timestampValue": testTimestamp,
115115
},
116116
"stringData": map[string]any{
117117
"stringValue": "Hello World",
@@ -150,7 +150,7 @@ var TestFirebaseDocFields = []map[string]any{
150150

151151
map[string]any{
152152
"timeData": map[string]any{
153-
"timestampValue": "2025-04-14T01:02:03Z",
153+
"timestampValue": testTimestamp,
154154
},
155155
},
156156
map[string]any{
@@ -206,7 +206,7 @@ var TestFirebaseDocFields = []map[string]any{
206206
// it includes a nested map that contains an array containing both a map and another nested array
207207
{
208208
"timeData": map[string]any{
209-
"timestampValue": "2025-04-14T01:02:03Z",
209+
"timestampValue": testTimestamp,
210210
},
211211
"stringData": map[string]any{
212212
"stringValue": "Hello World",
@@ -245,7 +245,7 @@ var TestFirebaseDocFields = []map[string]any{
245245
"mapValue": map[string]any{
246246
"fields": map[string]any{
247247
"timeData": map[string]any{
248-
"timestampValue": "2025-04-14T01:02:03Z",
248+
"timestampValue": testTimestamp,
249249
},
250250
"stringData": map[string]any{
251251
"stringValue": "Hello World",
@@ -283,7 +283,7 @@ var TestFirebaseDocFields = []map[string]any{
283283
"values": []any{
284284
map[string]any{
285285
"timeData": map[string]any{
286-
"timestampValue": "2025-04-14T01:02:03Z",
286+
"timestampValue": testTimestamp,
287287
},
288288
},
289289
map[string]any{
@@ -337,7 +337,7 @@ var TestFirebaseDocFields = []map[string]any{
337337
},
338338
map[string]any{
339339
"timeData": map[string]any{
340-
"timestampValue": "2025-04-14T01:02:03Z",
340+
"timestampValue": testTimestamp,
341341
},
342342
},
343343
map[string]any{
@@ -396,7 +396,7 @@ var TestFirebaseDocFields = []map[string]any{
396396

397397
var FlattenedMapResults = []map[string]any{
398398
{
399-
"timeData": "2025-04-14T01:02:03Z",
399+
"timeData": testTimestamp,
400400
},
401401
{
402402
"stringData": "Hello World",
@@ -426,7 +426,7 @@ var FlattenedMapResults = []map[string]any{
426426
"geoPointData": latlng.LatLng{Latitude: 51.205005708080876, Longitude: 3.225345050850536},
427427
},
428428
{
429-
"timeData": "2025-04-14T01:02:03Z",
429+
"timeData": testTimestamp,
430430
"stringData": "Hello World",
431431
"uuidData": "1f117a40-8bdb-4e8a-8f24-1622fea695b1",
432432
"boolData": true,
@@ -440,7 +440,7 @@ var FlattenedMapResults = []map[string]any{
440440
{
441441
"ArrayData": []any{
442442
map[string]any{
443-
"timeData": "2025-04-14T01:02:03Z",
443+
"timeData": testTimestamp,
444444
},
445445
map[string]any{
446446
"stringData": "Hello World",
@@ -473,7 +473,7 @@ var FlattenedMapResults = []map[string]any{
473473
},
474474

475475
{
476-
"timeData": "2025-04-14T01:02:03Z",
476+
"timeData": testTimestamp,
477477
"stringData": "Hello World",
478478
"uuidData": "1f117a40-8bdb-4e8a-8f24-1622fea695b1",
479479
"boolData": true,
@@ -486,7 +486,7 @@ var FlattenedMapResults = []map[string]any{
486486
"nestedMapData": map[string]any{
487487
"nestedArrayData": []any{
488488
map[string]any{
489-
"timeData": "2025-04-14T01:02:03Z",
489+
"timeData": testTimestamp,
490490
"stringData": "Hello World",
491491
"uuidData": "1f117a40-8bdb-4e8a-8f24-1622fea695b1",
492492
"boolData": true,
@@ -499,7 +499,7 @@ var FlattenedMapResults = []map[string]any{
499499
},
500500
map[string]any{
501501
"subNestedArrayData": []any{
502-
map[string]any{"timeData": "2025-04-14T01:02:03Z"},
502+
map[string]any{"timeData": testTimestamp},
503503
map[string]any{"stringData": "Hello World"},
504504
map[string]any{"uuidData": "1f117a40-8bdb-4e8a-8f24-1622fea695b1"},
505505
map[string]any{"boolData": true},
@@ -512,7 +512,7 @@ var FlattenedMapResults = []map[string]any{
512512
},
513513
},
514514
map[string]any{
515-
"timeData": "2025-04-14T01:02:03Z",
515+
"timeData": testTimestamp,
516516
},
517517
map[string]any{
518518
"stringData": "Hello World",
@@ -566,7 +566,7 @@ var StructResults = []any{
566566
"nestedArrayData": []any{
567567
map[string]any{
568568

569-
"timeData": "2025-04-14T01:02:03Z",
569+
"timeData": testTimestamp,
570570
"stringData": "Hello World",
571571
"uuidData": "1f117a40-8bdb-4e8a-8f24-1622fea695b1",
572572
"boolData": true,
@@ -579,7 +579,7 @@ var StructResults = []any{
579579
},
580580
map[string]any{
581581
"subNestedArrayData": []any{
582-
map[string]any{"timeData": "2025-04-14T01:02:03Z"},
582+
map[string]any{"timeData": testTimestamp},
583583
map[string]any{"stringData": "Hello World"},
584584
map[string]any{"uuidData": "1f117a40-8bdb-4e8a-8f24-1622fea695b1"},
585585
map[string]any{"boolData": true},
@@ -592,7 +592,7 @@ var StructResults = []any{
592592
},
593593
},
594594
map[string]any{
595-
"timeData": "2025-04-14T01:02:03Z",
595+
"timeData": testTimestamp,
596596
},
597597
map[string]any{
598598
"stringData": "Hello World",
@@ -639,7 +639,7 @@ var StructResults = []any{
639639
"nestedArrayData": []any{
640640
map[string]any{
641641

642-
"timeData": "2025-04-14T01:02:03Z",
642+
"timeData": testTimestamp,
643643
"stringData": "Hello World",
644644
"uuidData": "1f117a40-8bdb-4e8a-8f24-1622fea695b1",
645645
"boolData": true,
@@ -652,7 +652,7 @@ var StructResults = []any{
652652
},
653653
map[string]any{
654654
"subNestedArrayData": []any{
655-
map[string]any{"timeData": "2025-04-14T01:02:03Z"},
655+
map[string]any{"timeData": testTimestamp},
656656
map[string]any{"stringData": "Hello World"},
657657
map[string]any{"uuidData": "1f117a40-8bdb-4e8a-8f24-1622fea695b1"},
658658
map[string]any{"boolData": true},
@@ -665,7 +665,7 @@ var StructResults = []any{
665665
},
666666
},
667667
map[string]any{
668-
"timeData": "2025-04-14T01:02:03Z",
668+
"timeData": testTimestamp,
669669
},
670670
map[string]any{
671671
"stringData": "Hello World",

to_struct.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,23 @@ func dataToReflectPointer(p reflect.Value, data any) error {
9191
}
9292

9393
case typeOfGoTime:
94-
x, ok := data.(string)
95-
if !ok {
96-
return typeErr()
97-
}
94+
switch x := data.(type) {
95+
case time.Time:
96+
p.Set(reflect.ValueOf(x))
97+
return nil
9898

99-
ts, err := time.Parse(time.RFC3339, x)
100-
if err != nil {
101-
return typeErr()
102-
}
99+
case string:
100+
ts, err := time.Parse(time.RFC3339, x)
101+
if err != nil {
102+
return typeErr()
103+
}
103104

104-
p.Set(reflect.ValueOf(ts))
105-
return nil
105+
p.Set(reflect.ValueOf(ts))
106+
return nil
106107

108+
default:
109+
return typeErr()
110+
}
107111
case typeOfLatLng:
108112
switch x := data.(type) {
109113
case latlng.LatLng:

to_unwrapped.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"reflect"
2222
"strconv"
23+
"time"
2324

2425
"google.golang.org/genproto/googleapis/type/latlng"
2526
)
@@ -56,7 +57,6 @@ var FirestoreSimpleDataTypes = []string{
5657
protoStringTag,
5758
protoBoolTag,
5859
protoReferenceTag,
59-
protoTimestampTag,
6060
protoNullTag,
6161
}
6262

@@ -165,6 +165,11 @@ func unwrapFlatValue(value any) (any, error) {
165165
return unwrapGeoPoint(geoPointValue)
166166
}
167167

168+
// Ensure timestamp values are converted from map[string]interface{} to time.Time
169+
if timestampValue, ok := mapValue[protoTimestampTag]; ok {
170+
return unwrapTimestamp(timestampValue)
171+
}
172+
168173
for _, key := range FirestoreSimpleDataTypes {
169174
subValue, ok := mapValue[key]
170175
if !ok {
@@ -279,7 +284,7 @@ func unwrapInt(intValue any) (int, error) {
279284

280285
return decoded, nil
281286
}
282-
287+
283288
if iv, ok := intValue.(float32); ok {
284289
return int(iv), nil
285290
}
@@ -364,3 +369,29 @@ func unwrapGeoPoint(geoPointValue any) (latlng.LatLng, error) {
364369

365370
return latlng.LatLng{Latitude: lat, Longitude: lng}, nil
366371
}
372+
373+
// unwrapTimestamp converts timestamp values from map[string]interface{} to time.Time
374+
func unwrapTimestamp(timestampValue any) (time.Time, error) {
375+
if timestampValue == nil {
376+
return time.Time{}, nil
377+
}
378+
379+
// check if the timestampValue is already a time.Time type, if so, return it directly
380+
if ts, ok := timestampValue.(time.Time); ok {
381+
return ts, nil
382+
}
383+
384+
// we allow timestamp values to be encoded as a string in RFC3339 format, otherwise it is considered invalid
385+
t, ok := timestampValue.(string)
386+
if !ok {
387+
return time.Time{}, fmt.Errorf("unwrapTimestamp error processing timestamp value: %v", timestampValue)
388+
}
389+
390+
// Parse the Firestore timestamp string into a time.Time object
391+
parsedTime, err := time.Parse(time.RFC3339, t)
392+
if err != nil {
393+
return time.Time{}, fmt.Errorf("unwrapTimestamp error parsing timestamp string: %v", err)
394+
}
395+
396+
return parsedTime, nil
397+
}

0 commit comments

Comments
 (0)