-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodel.go
More file actions
255 lines (223 loc) Β· 6.37 KB
/
Copy pathmodel.go
File metadata and controls
255 lines (223 loc) Β· 6.37 KB
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package mongoose
import (
"context"
"fmt"
"log"
"reflect"
"slices"
"strings"
"time"
"github.com/tinh-tinh/tinhtinh/v2/common"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type ModelCommon interface {
SetConnect(connect *Connect)
GetName() string
}
type Model[M any] struct {
option *ModelOptions
docs []bson.E
connect *Connect
indexes []mongo.IndexModel
preHooks []Hook[M]
postHooks []Hook[M]
Ctx context.Context
Collection *mongo.Collection
}
type ModelOptions struct {
Timestamp bool
ID bool
Validation bool
StrictFilters bool // When true, rejects filters containing MongoDB operators
Indexes []mongo.IndexModel
}
// NewModel returns a new instance of Model[M] with the given connect and name
// name is the name of the collection in the database
// the returned Model[M] is used to interact with the collection in the database
func NewModel[M any](opts ...ModelOptions) *Model[M] {
defaultOption := ModelOptions{
ID: true,
Timestamp: true,
Validation: true,
}
if len(opts) > 0 {
defaultOption = common.MergeStruct(opts...)
}
return &Model[M]{
option: &defaultOption,
indexes: defaultOption.Indexes,
}
}
// SetConnect sets the context and collection of the model to the given connect.
// It is used internally by the ForFeature function to set the connect of the model.
// The given connect must be a *Connect.
func (m *Model[M]) SetConnect(connect *Connect) {
m.Ctx = connect.Ctx
m.connect = connect
m.Collection = connect.Client.Database(connect.DB).Collection(m.GetName())
if len(m.indexes) > 0 {
_, err := m.Collection.Indexes().CreateMany(m.Ctx, m.indexes)
if err != nil {
log.Println(err)
}
}
}
func (m *Model[M]) SetContext(ctx context.Context) {
m.Ctx = ctx
}
// GetName returns the name of the collection in the database
// Uses cached type info to avoid repeated reflection calls
func (m *Model[M]) GetName() string {
return GetCachedCollectionName[M]()
}
func (m *Model[M]) Index(idx bson.D, opt *options.IndexOptions) {
indexModel := mongo.IndexModel{
Keys: idx,
Options: opt,
}
m.indexes = append(m.indexes, indexModel)
}
// Set sets the data of the model to the given data.
// It iterates over the struct fields of the given data and sets the corresponding field of the model to the value of the field.
// If the field is not found in the model, it is not set.
// If the field is not tagged with bson, the field name is used as the key.
// If the field is tagged with bson, the tag value is used as the key.
// The given data must be a struct.
// Uses cached type info to reduce reflection overhead.
func (m *Model[M]) Set(data interface{}) {
typeInfo := GetTypeInfo[M]()
ctInput := reflect.ValueOf(data).Elem()
for i := range ctInput.NumField() {
name := ctInput.Type().Field(i).Name
val := ctInput.Field(i).Interface()
if val != nil {
// Use FieldsByName for lookup (includes promoted fields from embedded structs)
if field, exists := typeInfo.FieldsByName[name]; exists && field.BsonTag != "" {
m.docs = append(m.docs, bson.E{Key: field.BsonTag, Value: val})
}
}
}
}
// Save saves the changes to the model in the database.
// If the model has no ID, InsertOne is used to insert the document.
// If the model has an ID, UpdateByID is used to update the existing document.
// The createdAt and updatedAt fields are automatically set if not present.
// If the model has no changes, Save does nothing.
// Save returns an error if the operation fails.
func (m *Model[M]) Save() error {
err := ExecutePreHook(Save, m, m.docs)
if err != nil {
return err
}
if len(m.docs) == 0 {
return nil
}
idIndex := slices.IndexFunc(m.docs, func(e bson.E) bool {
return e.Key == "_id"
})
if idIndex == -1 {
inserts := m.docs
if m.option.ID {
inserts = append(m.docs,
bson.E{Key: "_id", Value: primitive.NewObjectID()},
)
}
if m.option.Timestamp {
inserts = append(m.docs,
bson.E{Key: "createdAt", Value: time.Now()},
bson.E{Key: "updatedAt", Value: time.Now()},
)
}
_, err := m.Collection.InsertOne(m.Ctx, inserts)
if err != nil {
return err
}
} else {
id := m.docs[idIndex].Value
updates := append(m.docs[:idIndex], m.docs[idIndex+1:]...)
if m.option.Timestamp {
updates = append(updates, bson.E{Key: "updatedAt", Value: time.Now()})
}
_, err := m.Collection.UpdateByID(m.Ctx, id, bson.D{{Key: "$set", Value: updates}})
if err != nil {
return err
}
}
err = ExecutePostHook(Save, m, m.docs)
if err != nil {
return err
}
m.docs = nil
return nil
}
func (m *Model[M]) Pre(nameStr HookName, hookFnc HookFnc[M], async ...bool) {
names := strings.Split(string(nameStr), "|")
if len(async) == 0 {
async = append(async, false)
}
for _, name := range names {
m.preHooks = append(m.preHooks, Hook[M]{
Name: HookName(name),
Func: hookFnc,
Async: async[0],
})
}
}
func (m *Model[M]) Post(nameStr HookName, hookFnc HookFnc[M], async ...bool) {
names := strings.Split(string(nameStr), "|")
if len(async) == 0 {
async = append(async, false)
}
for _, name := range names {
m.postHooks = append(m.postHooks, Hook[M]{
Name: HookName(name),
Func: hookFnc,
Async: async[0],
})
}
}
// ToDoc converts an interface{} to a bson.D, suitable for use with the bson and mongo packages.
// If the input is nil, ToDoc returns an empty bson.D.
// ToDoc returns an error if the input cannot be marshaled.
func ToDoc(v interface{}) (doc *bson.D, err error) {
if v == nil {
return &bson.D{}, nil
}
data, err := bson.Marshal(v)
if err != nil {
return
}
err = bson.Unmarshal(data, &doc)
return
}
// sanitizeFilter checks if the filter contains dangerous MongoDB operators
// when StrictFilters is enabled on the model.
func (m *Model[M]) sanitizeFilter(filter interface{}) error {
if m.option.StrictFilters {
return SanitizeFilter(filter)
}
return nil
}
func (m *Model[M]) getQueryId(id interface{}) (bson.M, error) {
var query bson.M
if m.option.ID {
switch v := id.(type) {
case string:
objId, err := primitive.ObjectIDFromHex(id.(string))
if err != nil {
return nil, err
}
query = bson.M{"_id": objId}
case primitive.ObjectID:
query = bson.M{"_id": id}
default:
return nil, fmt.Errorf("not support type %v", v)
}
} else {
query = bson.M{"_id": id}
}
return query, nil
}