-
Notifications
You must be signed in to change notification settings - Fork 3
/
callbacks.go
268 lines (229 loc) · 6.39 KB
/
callbacks.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
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
256
257
258
259
260
261
262
263
264
265
266
267
268
package fire
import (
"errors"
"reflect"
"gopkg.in/mgo.v2/bson"
)
// A Callback can be an Authorizer or Validator and is called during execution of
// a controller.
//
// Note: If the callback returns an error wrapped using Fatal() the API returns
// an InternalServerError status and the error will be logged. All other errors
// are serialized to an error object and returned.
type Callback func(*Context) error
type fatalError struct {
err error
}
// Fatal wraps an error and marks it as fatal.
func Fatal(err error) error {
return &fatalError{
err: err,
}
}
func (err *fatalError) Error() string {
return err.err.Error()
}
func isFatal(err error) bool {
_, ok := err.(*fatalError)
return ok
}
// Only will return a callback that runs the specified callback only when one
// of the supplied actions match.
func Only(cb Callback, actions ...Action) Callback {
return func(ctx *Context) error {
// check action
for _, a := range actions {
if a == ctx.Action {
return cb(ctx)
}
}
return nil
}
}
// Except will return a callback that runs the specified callback only when none
// of the supplied actions match.
func Except(cb Callback, actions ...Action) Callback {
return func(ctx *Context) error {
// check action
for _, a := range actions {
if a == ctx.Action {
return nil
}
}
return cb(ctx)
}
}
// ModelValidator performs a validation of the model using the Validate function.
func ModelValidator() Callback {
return func(ctx *Context) error {
// only run validator on Create and Update
if ctx.Action != Create && ctx.Action != Update {
return nil
}
// TODO: Add error source pointer.
return Validate(ctx.Model)
}
}
// ProtectedAttributesValidator compares protected attributes against their
// default (during Create) or stored value (during Update) and returns and
// error if they have been changed.
//
// Attributes are defined by passing pairs of fields and default values:
//
// ProtectedAttributesValidator(fire.Map{
// "title": "A fixed title",
// })
//
func ProtectedAttributesValidator(attributes map[string]interface{}) Callback {
return func(ctx *Context) error {
// only run validator on Create and Update
if ctx.Action != Create && ctx.Action != Update {
return nil
}
if ctx.Action == Create {
// check all attributes
for field, def := range attributes {
if !reflect.DeepEqual(ctx.Model.MustGet(field), def) {
return errors.New("Field " + field + " is protected")
}
}
}
if ctx.Action == Update {
// read the original
original, err := ctx.Original()
if err != nil {
return err
}
// check all attributes
for field := range attributes {
if !reflect.DeepEqual(ctx.Model.MustGet(field), original.MustGet(field)) {
return errors.New("Field " + field + " is protected")
}
}
}
return nil
}
}
// DependentResourcesValidator counts documents in the supplied collections
// and returns an error if some get found. This callback is meant to protect
// resources from breaking relations when requested to be deleted.
//
// Resources are defined by passing pairs of collections and fields where the
// field must be a database field of the target resource model:
//
// DependentResourcesValidator(fire.Map{
// "posts": "user_id",
// "comments": "user_id",
// })
//
func DependentResourcesValidator(resources map[string]string) Callback {
return func(ctx *Context) error {
// only run validator on Delete
if ctx.Action != Delete {
return nil
}
// check all relations
for coll, field := range resources {
// count referencing documents
n, err := ctx.Store.DB().C(coll).Find(bson.M{
field: ctx.Query["_id"],
}).Limit(1).Count()
if err != nil {
return Fatal(err)
}
// immediately return if a document is found
if n == 1 {
return errors.New("Resource has dependent resources")
}
}
// pass validation
return nil
}
}
// VerifyReferencesValidator makes sure all references in the document are
// existing by counting on the related collections.
//
// References are defined by passing pairs of fields and collections where the
// field must be a database field on the resource model:
//
// VerifyReferencesValidator(fire.Map{
// "post_id": "posts",
// "user_id": "users",
// })
//
func VerifyReferencesValidator(references map[string]string) Callback {
return func(ctx *Context) error {
// only run validator on Create and Update
if ctx.Action != Create && ctx.Action != Update {
return nil
}
// check all references
for field, collection := range references {
// read referenced id
id := ctx.Model.MustGet(field)
// continue if reference is not set
if oid, ok := id.(*bson.ObjectId); ok && oid == nil {
continue
}
// count entities in database
n, err := ctx.Store.DB().C(collection).FindId(id).Limit(1).Count()
if err != nil {
return Fatal(err)
}
// check for existence
if n != 1 {
return errors.New("Missing required relationship " + field)
}
}
// pass validation
return nil
}
}
// MatchingReferencesValidator compares the model with a related model and
// checks if certain references are shared.
//
// The target model is defined by passing its collection and the referencing
// field on the current model. The matcher is defined by passing pairs of
// database fields on the target and current model:
//
// MatchingReferencesValidator("posts", "post_id", fire.Map{
// "user_id": "user_id",
// })
//
func MatchingReferencesValidator(collection, reference string, matcher map[string]string) Callback {
return func(ctx *Context) error {
// only run validator on Create and Update
if ctx.Action != Create && ctx.Action != Update {
return nil
}
// get main reference
id := ctx.Model.MustGet(reference)
// continue if reference is not set
if oid, ok := id.(*bson.ObjectId); ok && oid == nil {
return nil
}
// prepare query
query := bson.M{
"_id": id,
}
// add other references
for targetField, modelField := range matcher {
id := ctx.Model.MustGet(modelField)
// abort if reference is missing
if oid, ok := id.(*bson.ObjectId); ok && oid == nil {
return errors.New("Missing ID")
}
query[targetField] = id
}
// query db
n, err := ctx.Store.DB().C(collection).Find(query).Limit(1).Count()
if err != nil {
return Fatal(err)
}
// return error if document is missing
if n == 0 {
return errors.New("References do not match")
}
return nil
}
}