UNPKG

9.13 kBPlain TextView Raw
1require! {
2 async
3 underscore: _
4 validator: Validator
5 \../Helpers/Debug
6 \../../ : N
7}
8
9global import require \prelude-ls
10
11validationError = (field, value, message) ->
12 field: field
13 value: value
14 message: "The '#field' field #{if value? => "with value '#value'"} #message"
15
16typeCheck =
17 bool: (value) -> typeof(value) isnt 'string' and '' + value is 'true' or '' + value is 'false'
18 int: is-type \Number
19 float: is-type \Number
20 string: is-type \String
21 date: Validator.isDate
22 email: Validator.isEmail
23 array: (value) -> Array.isArray value
24 arrayOf: (type) -> (value) ~> not _(map (@[type]), value).contains (item) -> item is false
25
26class SchemaProperty
27
28 name: null
29 default: null
30 unique: false
31 optional: true
32 internal: false
33 unique: false
34 type: null
35 validation: null
36
37 (@name, @type, @optional) ->
38 throw new Error 'SchemaProperty must have a name' if not @name?
39
40 if is-type \Array @type
41 @validation = typeCheck.arrayOf @type.0
42 else
43 @validation = typeCheck[@type]
44
45 Default: (@default) -> @
46 Unique: (@unique = true) -> @
47 Optional: (@optional = true) -> @
48 Required: (required = true) -> @optional = !required; @
49 Virtual: (@virtual = null) -> @
50 Internal: (@internal = true) -> @
51 Unique: (@unique = true) -> @
52
53class Schema
54
55 mode: null
56
57 (@name, @mode = 'free') ->
58 @properties = []
59 @assocs = []
60 @habtm = []
61 @debug = new Debug "N::Resource::#{@name}::Schema", Debug.colors.cyan
62
63 Populate: (instance, blob) ->
64
65 res = obj-to-pairs blob |> filter (.0.0 isnt \_) |> pairs-to-obj
66 instance <<< res
67
68 @properties
69 |> each (-> instance[it.name] =
70 | blob[it.name]? => that
71 | it.default? => switch
72 | is-type \Function it.default => it.default!
73 | _ => it.default
74 | _ => void)
75
76 @assocs |> each -> instance[it.name] = that if blob[it.name]?
77
78 @properties
79 |> filter (.virtual?)
80 |> each ~>
81 try
82 result = it.virtual.call instance, instance, (val) ~>
83 instance[it.name] = val
84
85 if result?
86 instance[it.name] = result
87 catch e
88 instance[it.name] = undefined
89
90 instance
91
92 Filter: (instance) ->
93
94 res = {}
95
96 if @mode is \strict
97 res.id = instance.id
98 @properties
99 |> filter (-> not it.virtual?)
100 |> each -> res[it.name] = instance[it.name]
101 else
102 each (~>
103 if it[0] isnt \_ and typeof! instance[it] isnt 'Function' and
104 it not in map (.name), @assocs and
105 not _(@properties).findWhere(name: it)?.virtual? and
106 (typeof! instance[it]) isnt 'Object' and
107 (typeof! instance[it]) isnt 'Array'
108
109 res[it] = instance[it]), keys instance
110
111 res
112
113 RemoveInternals: (blob) ->
114 @properties
115 |> filter (.internal)
116 |> each -> delete blob[it.name]
117 blob
118
119 Field: (name, type) ->
120 return that if _(@properties).findWhere name: name
121
122 @properties.push new SchemaProperty name, type, @mode is 'free'
123 @properties[*-1]
124
125 # Check for schema validity
126 Validate: (blob, done) ->
127 delete blob._id if N.config.dbType is \Mongo
128
129 errors = []
130
131 @properties
132 |> each ~>
133 errors := errors.concat @_CheckPresence blob, it
134 errors := errors.concat @_CheckValid blob, it
135
136 errors = errors.concat @_CheckNotInSchema blob
137 @_CheckUnique blob, @Resource, (err, results) ->
138 if err?
139 errors := errors.concat results
140 done(if errors.length => {errors} else null)
141
142
143 GetVirtuals: (instance, blob) ->
144 res = {}
145 (@properties or [])
146 |> filter (.virtual?)
147 |> each (-> res[it.name] = it.virtual.call instance, blob, ->)
148
149 res
150
151 _CheckPresence: (blob, property) ->
152 if !property.optional and not property.default? and not blob[property.name]? and not property.virtual?
153 [validationError property.name, blob[property.name], ' was not present.']
154 else
155 []
156
157 _CheckValid: (blob, property) ->
158 if blob[property.name]? and not (property.validation)(blob[property.name])
159 [validationError property.name,
160 blob[property.name],
161 ' was not a valid ' + property.type]
162 else
163 []
164 _CheckNotInSchema: (blob) ->
165 return [] if @mode is \free
166
167 for field, value of blob when not _(@properties).findWhere name: field and field isnt \id
168 validationError field, blob[field], ' is not in schema'
169
170 _CheckUnique: (blob, Resource, done) ->
171 res = []
172 async.eachSeries filter((.unique), @properties), (property, done) ->
173 Resource.Fetch (property.name): blob[property.name]
174 .Then ->
175 res.push validationError property.name, blob[property.name], ' must be unique'
176 done {err: 'not unique'}
177 .Catch -> done!
178 , (err, results) -> done err, res
179
180 PrepareRelationship: (isArray, field, description) ->
181 type = null
182 foreign = null
183 get = (blob, done) ->
184 done new Error 'No local or distant key given'
185
186 # debug-res.Log "Preparing Relationships with #{description.type.name}"
187
188 if description.localKey?
189 keyType = \local
190 foreign = description.localKey
191 get = (blob, done, _depth) ->
192 if _depth < 0
193 return done()
194
195 if !isArray
196 if not typeCheck.int blob[description.localKey]
197 return done new Error 'Model association needs integer as id and key'
198 else
199 if not typeCheck.array blob[description.localKey]
200 return done new Error 'Model association needs array of integer as ids and localKeys'
201
202 description.type._FetchUnwrapped blob[description.localKey], done, _depth
203
204 else if description.distantKey?
205 foreign = description.distantKey
206 keyType = \distant
207 get = (blob, done, _depth) ->
208 if _depth < 0 or not blob.id?
209 return done()
210
211 if !isArray
212 description.type._FetchUnwrapped {"#{description.distantKey}": blob.id} , done, _depth
213 else
214 description.type._ListUnwrapped {"#{description.distantKey}": blob.id}, done, _depth
215
216 toPush =
217 keyType: keyType
218 type: description.type
219 name: field
220 Get: get
221 foreign: foreign
222 toPush.default = description.default if description.default?
223 @assocs.push toPush
224
225 # Get each associated Resource
226 FetchAssoc: (blob, done, _depth) ->
227 assocs = {}
228
229 @debug.Log "Fetching #{@assocs.length} assocs with Depth #{_depth}"
230 async.eachSeries @assocs, (resource, _done) ~>
231 done = (err, data)->
232 _done err, data
233
234 @debug.Log "Assoc: Fetching #{resource.name}"
235 resource.Get blob, (err, instance) ->
236 assocs[resource.name] = resource.default if resource.default?
237
238 if err? and resource.type is \distant => done!
239 else
240 assocs[resource.name] = instance if instance?
241 done!
242 , _depth
243 , (err) ->
244 return done err if err?
245
246 done null, _.extend blob, assocs
247
248 HasOneThrough: (res, through) ->
249 get = (blob, done, _depth) ~>
250 return done! if not _depth or not blob.id?
251
252 assoc = _(@assocs).findWhere name: capitalize through.lname
253 assoc.Get blob, (err, instance) ->
254 return done err if err?
255
256 done null, instance[capitalize res.lname]
257 , _depth + 1
258
259 toPush =
260 keyType: 'distant'
261 type: res
262 name: capitalize res.lname
263 Get: get
264 @assocs.push toPush
265
266 HasManyThrough: (res, through) ->
267 get = (blob, done, _depth) ~>
268 return done! if not _depth or not blob.id?
269
270 assoc = _(@assocs).findWhere name: capitalize through.lname + \s
271 assoc.Get blob, (err, instance) ->
272 return done err if err?
273
274 res._ListUnwrapped instance[capitalize res.lname], (err, instances) ->
275 return done err if err?
276
277 done null, instances
278 , depth
279
280 , _depth
281
282 toPush =
283 keyType: 'distant'
284 type: res
285 name: capitalize res.lname + \s
286 Get: get
287 @assocs.push toPush
288
289 HasAndBelongsToMany: (res, through) ->
290 get = (blob, done, _depth) ~>
291 return done! if not _depth or not blob.id?
292
293 through._ListUnwrapped "#{@name + \Id }": blob.id, (err, instances) ~>
294 return done err if err?
295
296 async.mapSeries instances, (instance, done) ~>
297 res._FetchUnwrapped instance[res.lname + \Id ], done, _depth - 1
298 , (err, results) ~>
299 return done err if err?
300
301 assocs =
302 | results.length => results
303 | _ => null
304
305 done null, assocs
306
307 , _depth
308
309 toPush =
310 keyType: 'distant'
311 type: res
312 name: capitalize res.lname + \s
313 Get: get
314 @assocs.push toPush
315 @habtm.push through
316 # console.log @habtm
317
318 Inherit: ->
319 properties: @properties
320 |> map ->
321 sp = new SchemaProperty it.name, it.type, it.optional
322 sp <<< it
323 assocs: map (-> _ {} .extend it), @assocs
324 habtm: map (-> _ {} .extend it), @habtm
325
326
327module.exports = Schema