1 | require! {
|
2 | async
|
3 | underscore: _
|
4 | validator: Validator
|
5 | \../Helpers/Debug
|
6 | \../../ : N
|
7 | }
|
8 |
|
9 | global import require \prelude-ls
|
10 |
|
11 | validationError = (field, value, message) ->
|
12 | field: field
|
13 | value: value
|
14 | message: "The '#field' field #{if value? => "with value '#value'"} #message"
|
15 |
|
16 | typeCheck =
|
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 |
|
26 | class 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 |
|
53 | class 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
327 | module.exports = Schema
|