-
Notifications
You must be signed in to change notification settings - Fork 8
/
hour_firestore_repository.go
222 lines (184 loc) · 5.92 KB
/
hour_firestore_repository.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package adapters
import (
"context"
"time"
"cloud.google.com/go/firestore"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour"
)
type (
DateModel struct {
Date time.Time `firestore:"Date"`
HasFreeHours bool `firestore:"HasFreeHours"`
Hours []HourModel `firestore:"Hours"`
}
HourModel struct {
Available bool `firestore:"Available"`
HasTrainingScheduled bool `firestore:"HasTrainingScheduled"`
Hour time.Time `firestore:"Hour"`
}
)
type FirestoreHourRepository struct {
firestoreClient *firestore.Client
hourFactory hour.Factory
}
func NewFirestoreHourRepository(firestoreClient *firestore.Client, hourFactory hour.Factory) *FirestoreHourRepository {
if firestoreClient == nil {
panic("missing firestoreClient")
}
if hourFactory.IsZero() {
panic("missing hourFactory")
}
return &FirestoreHourRepository{firestoreClient, hourFactory}
}
func (f FirestoreHourRepository) GetHour(ctx context.Context, time time.Time) (*hour.Hour, error) {
date, err := f.getDateDTO(
// getDateDTO should be used both for transactional and non transactional query,
// the best way for that is to use closure
func() (doc *firestore.DocumentSnapshot, err error) {
return f.documentRef(time).Get(ctx)
},
time,
)
if err != nil {
return nil, err
}
hourFromDb, err := f.domainHourFromDateDTO(date, time)
if err != nil {
return nil, err
}
return hourFromDb, err
}
func (f FirestoreHourRepository) UpdateHour(
ctx context.Context,
hourTime time.Time,
updateFn func(h *hour.Hour) (*hour.Hour, error),
) error {
err := f.firestoreClient.RunTransaction(ctx, func(ctx context.Context, transaction *firestore.Transaction) error {
dateDocRef := f.documentRef(hourTime)
firebaseDate, err := f.getDateDTO(
// getDateDTO should be used both for transactional and non transactional query,
// the best way for that is to use closure
func() (doc *firestore.DocumentSnapshot, err error) {
return transaction.Get(dateDocRef)
},
hourTime,
)
if err != nil {
return err
}
hourFromDB, err := f.domainHourFromDateDTO(firebaseDate, hourTime)
if err != nil {
return err
}
updatedHour, err := updateFn(hourFromDB)
if err != nil {
return errors.Wrap(err, "unable to update hour")
}
updateHourInDataDTO(updatedHour, &firebaseDate)
return transaction.Set(dateDocRef, firebaseDate)
})
return errors.Wrap(err, "firestore transaction failed")
}
func (f FirestoreHourRepository) trainerHoursCollection() *firestore.CollectionRef {
return f.firestoreClient.Collection("trainer-hours")
}
func (f FirestoreHourRepository) documentRef(hourTime time.Time) *firestore.DocumentRef {
return f.trainerHoursCollection().Doc(hourTime.Format("2006-01-02"))
}
func (f FirestoreHourRepository) getDateDTO(
getDocumentFn func() (doc *firestore.DocumentSnapshot, err error),
dateTime time.Time,
) (DateModel, error) {
doc, err := getDocumentFn()
if status.Code(err) == codes.NotFound {
// in reality this date exists, even if it's not persisted
return NewEmptyDateDTO(dateTime), nil
}
if err != nil {
return DateModel{}, err
}
date := DateModel{}
if err := doc.DataTo(&date); err != nil {
return DateModel{}, errors.Wrap(err, "unable to unmarshal DateModel from Firestore")
}
return date, nil
}
// for now we are keeping backward comparability, because of that it's a bit messy and overcomplicated
// todo - we will clean it up later with CQRS :-)
func (f FirestoreHourRepository) domainHourFromDateDTO(date DateModel, hourTime time.Time) (*hour.Hour, error) {
firebaseHour, found := findHourInDateDTO(date, hourTime)
if !found {
// in reality this date exists, even if it's not persisted
return f.hourFactory.NewNotAvailableHour(hourTime)
}
availability, err := mapAvailabilityFromDTO(firebaseHour)
if err != nil {
return nil, err
}
return f.hourFactory.UnmarshalHourFromDatabase(firebaseHour.Hour.Local(), availability)
}
// for now we are keeping backward comparability, because of that it's a bit messy and overcomplicated
// todo - we will clean it up later with CQRS :-)
func updateHourInDataDTO(updatedHour *hour.Hour, firebaseDate *DateModel) {
firebaseHourDTO := domainHourToDTO(updatedHour)
hourFound := false
for i := range firebaseDate.Hours {
if !firebaseDate.Hours[i].Hour.Equal(updatedHour.Time()) {
continue
}
firebaseDate.Hours[i] = firebaseHourDTO
hourFound = true
break
}
if !hourFound {
firebaseDate.Hours = append(firebaseDate.Hours, firebaseHourDTO)
}
firebaseDate.HasFreeHours = false
for _, h := range firebaseDate.Hours {
if h.Available {
firebaseDate.HasFreeHours = true
break
}
}
}
func mapAvailabilityFromDTO(firebaseHour HourModel) (hour.Availability, error) {
if firebaseHour.Available && !firebaseHour.HasTrainingScheduled {
return hour.Available, nil
}
if !firebaseHour.Available && firebaseHour.HasTrainingScheduled {
return hour.TrainingScheduled, nil
}
if !firebaseHour.Available && !firebaseHour.HasTrainingScheduled {
return hour.NotAvailable, nil
}
return hour.Availability{}, errors.Errorf(
"unsupported values - Available: %t, HasTrainingScheduled: %t",
firebaseHour.Available,
firebaseHour.HasTrainingScheduled,
)
}
func domainHourToDTO(updatedHour *hour.Hour) HourModel {
return HourModel{
Available: updatedHour.IsAvailable(),
HasTrainingScheduled: updatedHour.HasTrainingScheduled(),
Hour: updatedHour.Time(),
}
}
func findHourInDateDTO(firebaseDate DateModel, time time.Time) (HourModel, bool) {
for i := range firebaseDate.Hours {
firebaseHour := firebaseDate.Hours[i]
if !firebaseHour.Hour.Equal(time) {
continue
}
return firebaseHour, true
}
return HourModel{}, false
}
func NewEmptyDateDTO(t time.Time) DateModel {
return DateModel{
Date: t.UTC().Truncate(time.Hour * 24),
}
}