· 8 years ago · Nov 16, 2017, 02:02 PM
1/*
2Copyright (C) 2016 Tony Mobily
3
4Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
6The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
8THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9*/
10
11/*
12NOTE. When creating a store, you can take the following shortcuts:
13 * Don't specify `paramIds`. If not specified, it will be worked out from publicURL
14 * Don't specify `idProperty`. If idProperty not specified, it will be assumed last element of paramIds
15 * Don't specify `paramIds` in schema. They will be added to the schema as `{type: 'id' }` automatically
16 * Don't specify `onlineSearchSchema`. It will be worked out taking all schema element marked as
17 `searchable: true` (except paramIds)
18*/
19
20var
21 e = require('allhttperrors'),
22 declare = require('simpledeclare'),
23 Schema = require('simpleschema'),
24 async = require('async'),
25 SimpleDbLayerMixin = require('./SimpleDbLayerMixin.js'),
26 HTTPMixin = require('./HTTPMixin.js'),
27 path = require('path'),
28 DO = require('deepobject'),
29 marked = require('marked')
30
31marked.setOptions({
32 gfm: true,
33 tables: true,
34 breaks: false,
35 pedantic: false,
36 sanitize: true,
37 smartLists: true,
38 smartypants: false
39})
40
41var _co = function (o) {
42 var newO = {}
43 for (var k in o) if (o.hasOwnProperty(k)) newO[ k ] = o[ k ]
44 return newO
45}
46
47var Store = declare(Object, {
48
49 // ***********************************************************
50 // *** ATTRIBUTES THAT ALWAYS NEED TO BE DEFINED IN PROTOTYPE
51 // ***********************************************************
52
53 storeName: null,
54 schema: null,
55 nested: [],
56 autoLookup: {},
57 _singleFields: {}, // Fields that can be updated singularly
58 _uniqueFields: {}, // Fields that absolutely must be unique
59
60 // ****************************************************
61 // *** ATTRIBUTES THAT CAN TO BE DEFINED IN PROTOTYPE
62 // ****************************************************
63
64 onlineSearchSchema: null, // If not set in prototype, worked out from `schema` by constructor
65 queryConditions: null, // If not set in prototype, worked out from `schema` by constructor
66 sortableFields: [],
67 publicURLprefix: null,
68 publicURL: null, // Not mandatory (if you want your store to be API-only for some reason)
69 idProperty: null, // If not set in prototype, taken as last item of paramIds)
70 paramIds: [], // Only allowed if publicURL is not set
71
72 // ****************************************************
73 // *** ATTRIBUTES THAT DEFINE STORE'S BEHAVIOUR
74 // ****************************************************
75
76 handlePut: false,
77 handlePost: false,
78 handleGet: false,
79 handleGetQuery: false,
80 handleDelete: false,
81
82 echoAfterPut: true,
83 echoAfterPost: true,
84 echoAfterDelete: true,
85
86 chainErrors: 'none', // can be 'none' (do not chain), 'all' (chain all), 'nonhttp' (chain non-HTTP errors)
87
88 deleteAfterGetQuery: false, // Delete records after fetching them
89
90 strictSchemaOnFetch: false,
91
92 // failWithProtectedFields: false,
93
94 position: false, // If set, will make fields re-positionable
95 defaultSort: null, // If set, it will be applied to all getQuery calls
96
97 // Methods that MUST be implemented for the store to be functional
98
99 implementFetchOne: function (request, cb) {
100 throw ('implementFetchOne not implemented, store is not functional')
101 },
102
103 implementInsert: function (request, forceId, cb) {
104 throw ('implementInsert not implemented, store is not functional')
105 },
106
107 implementUpdate: function (request, deleteUnsetFields, cb) {
108 throw ('implementUpdate not implemented, store is not functional')
109 },
110
111 implementDelete: function (request, cb) {
112 throw ('implementDelete not implemented, store is not functional')
113 },
114
115 implementQuery: function (request, next) {
116 throw ('implementQuery not implemented, store is not functional')
117 },
118
119 implementReposition: function (doc, where, beforeId, cb) {
120 throw ('implementReposition not implemented, store is not functional')
121 },
122
123 // ****************************************************
124 // *** FUNCTIONS THAT CAN BE OVERRIDDEN BY DEVELOPERS
125 // ****************************************************
126
127 // Doc extrapolation and preparation calls
128 prepareBody: function (request, method, body, cb) { cb(null, body) },
129 extrapolateDoc: function (request, method, doc, cb) { cb(null, doc) },
130 prepareBeforeSend: function (request, method, doc, cb) { cb(null, doc) },
131 manipulateQueryConditions: function (request, method, cb) { cb(null, this.queryConditions) },
132
133 // Permission stock functions
134 checkPermissions: function (request, method, cb) { cb(null, true) },
135
136 // after* functions
137 afterValidate: function (request, method, cb) { cb(null) },
138 afterCheckPermissions: function (request, method, cb) { cb(null) },
139 afterDbOperation: function (request, method, cb) { cb(null) },
140 afterEverything: function (request, method, cb) { cb(null) },
141
142 logError: function (error) { },
143
144 'error-format-doc': "{ message: 'The message', errors: [ { field1: 'message1', field2: 'message2' } ] } (errors is optional)",
145 formatErrorResponse: function (error) {
146 if (error.errors) {
147 return { message: error.message, errors: error.errors }
148 } else {
149 return { message: error.message }
150 }
151 },
152
153 // Run when JsonRestStores.init() is run
154 init: function () {
155 },
156
157 // **************************************************************************
158 // *** END OF FUNCTIONS/ATTRIBUTES THAT NEED/CAN BE OVERRIDDEN BY DEVELOPERS
159 // **************************************************************************
160
161 // Default error objects which might be used by this module.
162 BadRequestError: e.BadRequestError,
163 UnauthorizedError: e.UnauthorizedError,
164 ForbiddenError: e.ForbiddenError,
165 NotFoundError: e.NotFoundError,
166 PreconditionFailedError: e.PreconditionFailedError,
167 UnprocessableEntityError: e.UnprocessableEntityError,
168 NotImplementedError: e.NotImplementedError,
169 ServiceUnavailableError: e.ServiceUnavailableError,
170
171 constructor: function () {
172 var self = this
173
174 // Set artificialDelay from the constructor's default
175 this.artificialDelay = this.constructor.artificialDelay
176
177 if (typeof (Store.registry[ self.storeName ]) !== 'undefined') {
178 throw new Error('Cannot instantiate two stores with the same name: ' + self.storeName)
179 }
180
181 // The store name must be defined
182 if (self.storeName === null) {
183 throw (new Error('You must define a store name for a store in constructor class'))
184 }
185
186 // The schema must be defined
187 if (self.schema == null) {
188 throw (new Error('You must define a schema'))
189 }
190
191 // If paramId is not specified, takes it from publicURL
192 if (self.paramIds.length === 0 && typeof (self.publicURL) === 'string') {
193 self.paramIds = (self.publicURL + '/').match(/:.*?\/+/g).map(
194 function (i) {
195 return i.substr(1, i.length - 2)
196 }
197 )
198 }
199
200 // If idProperty is not set, derive it from self._lastParamId()
201 if (!self.idProperty) {
202 if (self.paramIds.length === 0) {
203 throw (new Error('Your store needs to set idProperty, or alternatively set paramIds (idProperty will be the last paramId). Store: ' + self.storeName))
204 }
205
206 // Sets self.idProperty, which (as for the principle of
207 // least surprise) must be the last paramId passed to
208 // the store.
209 self.idProperty = self._lastParamId()
210 }
211
212 // By default, paramIds are set in schema as { type: 'id' } so that developers
213 // can be lazy when defining their schemas
214 for (var i = 0, l = self.paramIds.length; i < l; i++) {
215 var k = self.paramIds[ i ]
216 if (typeof (self.schema.structure[ k ]) === 'undefined') {
217 self.schema.structure[ k ] = { type: 'id' }
218 }
219 }
220
221 // If onlineSearchSchema wasn't defined, then set it as a copy of the schema where
222 // fields are `searchable`, EXCLUDING the paramIds fields.
223 if (self.onlineSearchSchema == null) {
224 var onlineSearchSchemaStructure = { }
225 for (var k in self.schema.structure) {
226 if (self.schema.structure[ k ].searchable && self.paramIds.indexOf(k) === -1) {
227 onlineSearchSchemaStructure[ k ] = self.schema.structure[ k ]
228 }
229 }
230 self.onlineSearchSchema = new self.schema.constructor(onlineSearchSchemaStructure)
231 }
232
233 // If queryConditions is not defined, create one
234 // based on the onlineSearchSchema (each field is searchable)
235 if (self.queryConditions == null) {
236 // If onlineSearchSchema only has 1 element, there is no point in having an 'and'
237 var keys = Object.keys(self.onlineSearchSchema.structure)
238 if (keys.length === 1) {
239 var k = keys[ 0 ]
240 self.queryConditions = { type: 'eq', args: [ k, '#' + k + '#' ] }
241 } else {
242 self.queryConditions = { type: 'and', args: [ ] }
243 for (var k in self.onlineSearchSchema.structure) {
244 self.queryConditions.args.push({ type: 'eq', args: [ k, '#' + k + '#' ] })
245 }
246 }
247 }
248
249 self._singleFields = {}
250 for (var k in self.schema.structure) {
251 if (self.schema.structure[ k ].singleField) {
252 self._singleFields[ k ] = self.schema.structure[ k ]
253 }
254 }
255
256 self._uniqueFields = {}
257 for (var k in self.schema.structure) {
258 if (self.schema.structure[ k ].unique) {
259 self._uniqueFields[ k ] = self.schema.structure[ k ]
260 }
261 }
262
263 Store.registry[ self.storeName ] = self
264 },
265
266 // Simple function that shallow-copies an object. This should be used
267 // every time prepareBody, extrapolateDocProxy or prepareBeforeSendProxy are
268 // overridden (in order to pass a copy of the object)
269 _co: _co,
270
271 // This will call either `extrapolateDoc` or `prepareBeforeSend` (depending on
272 // the `funcName` parameter) on the document itself, _and_ on any nested documents
273 // in the record itself.
274 // Note that `extrapolateDoc` and `prepareBeforeSend` have identical signatures
275 _genericProcessProxy: function (funcName, request, method, doc, done) {
276 var self = this
277
278 self[ funcName ](self, request, method, doc, function (err, processedDoc) {
279 if (err) return done(err)
280
281 // No nested table: go home!
282 if (self.nested.length === 0) return done(null, processedDoc)
283
284 // For each nested table...
285 async.eachSeries(
286 self.nested,
287 function (n, cb) {
288 var store = n.store
289
290 switch (n.type) {
291 case 'multiple':
292
293 // The key will depend on the store's table's name or prop
294 var k = n.prop || store.dbLayer.table
295
296 // Not an array: not interested!
297 var a = processedDoc._children && processedDoc._children[ k ]
298 if (!Array.isArray(a) || !a.length) return cb(null)
299
300 // For each element in the array...
301 async.map(
302 a,
303 function (item, cb) {
304 var requestCopy = self._co(request)
305 requestCopy.nested = true
306 store[ funcName ](store, requestCopy, method, item, function (err, item) {
307 if (err) return cb(err)
308
309 cb(null, item)
310 })
311 },
312 function (err, resultA) {
313 // console.log("RESULT A:", resultA );
314 processedDoc._children[ k ] = resultA.filter((i) => { return Object.keys(i).length !== 0 })
315
316 cb(null)
317 }
318 )
319
320 break
321
322 case 'lookup':
323
324 // The key will depend on the localField or `prop`
325 var k = n.prop || n.localField
326
327 // Not an object: not interested!
328 var o = processedDoc._children && processedDoc._children[ k ]
329 if (typeof o === 'undefined') return cb(null)
330
331 var requestCopy = self._co(request)
332 requestCopy.nested = true
333
334 store[ funcName ](store, requestCopy, method, o, function (err, o) {
335 if (err) return cb(err)
336
337 processedDoc._children[ k ] = o
338 cb(null)
339 })
340
341 break
342 }// End of switch
343 },
344 function (err) {
345 done(null, processedDoc)
346 }
347 ) // async.each for nested table
348 })
349 },
350
351 extrapolateDocProxy: function (request, method, fullDoc, done) {
352 this._genericProcessProxy('extrapolateDoc', request, method, fullDoc, done)
353 },
354
355 prepareBeforeSendProxy: function (request, method, doc, done) {
356 this._genericProcessProxy('prepareBeforeSend', request, method, doc, done)
357 },
358
359 _doAutoLookup: function (request, method, done) {
360 var self = this
361 request.lookup = {}
362 async.each(
363 Object.keys(self.autoLookup),
364 function (id, cb) {
365 var storeName = self.autoLookup[ id ]
366 var store = Store.getStore(storeName)
367
368 // The parameter is not in the request (as in parameters, body or conditionsHash), nothing to do.
369 var v = request.params[ id ] || request.body[ id ] || (request.options.conditionsHash && request.options.conditionsHash[ id ])
370 if (!v) return cb(null)
371
372 var q = {}
373 q[ store.idProperty ] = v
374
375 store.dbLayer.selectByHash(q, function (err, docs, total) {
376 if (err) return cb(err)
377 if (total == 0) return cb(new self.NotFoundError('Not found: ' + id))
378 request.lookup[ id ] = docs[ 0 ]
379 cb(null)
380 })
381 },
382 function (err) {
383 if (err) return done(err)
384 done(null)
385 }
386 )
387 },
388
389 // Will call implementReposition based on options.
390 // -putBefore is an id.
391 // -putDefaultPosition is 'start' or 'end'.
392 // -existing is true or false: true for existing records false for new ones
393 //
394 // When calling implementReposition:
395 // - where can be 'start', 'end' or 'before'
396 // - beforeId is only meaningful for 'before' (tell is where to place it)
397 // - existing is boolean, and it's only meaningful if this.position is there
398 _repositionBasedOnOptions: function (fullDoc, putBefore, putDefaultPosition, existing, cb) {
399 // No position field: nothing to do
400 if (!this.position) {
401 return cb(null)
402 }
403
404 // CASE #1: putBefore is set: where = at, beforeId = putBefore
405 if (putBefore) {
406 this.implementReposition(fullDoc, 'before', putBefore, cb)
407
408 // CASE #2: putDefaultPosition is set: where = putDefaultPosition, beforeId = null
409 } else if (putDefaultPosition) {
410 this.implementReposition(fullDoc, putDefaultPosition, null, cb)
411
412 // CASE #3: putBefore and putDefaultPosition are not set. IF it's a new record, where = end, beforeId = null
413 } else if (!existing) {
414 this.implementReposition(fullDoc, 'end', null, cb)
415
416 // CASE #4: don't do anything.
417 } else {
418 cb(null)
419 }
420 },
421
422 getFullPublicURL: function () {
423 // No prefix: return the publicURL straight
424 if (!this.publicURLPrefix) return this.publicURL
425
426 return path.join(this.publicURLPrefix, this.publicURL)
427 },
428
429 // ****************************************************
430 // *** INTERNAL FUNCTIONS, DO NOT TOUCH
431 // ****************************************************
432
433 // This function is self-contained so that it can be easily checked
434 // and debugged.
435
436 _resolveQueryConditions: function (queryConditions, conditionsHash, allowedFields, request) {
437 var self = this
438
439 // Copy over conditionsHash and allowedFields so that they don't get polluted
440 // since things will possibly get added here
441 conditionsHash = JSON.parse(JSON.stringify(conditionsHash))
442 allowedFields = JSON.parse(JSON.stringify(allowedFields))
443
444 // Entry point for recursive function
445 // Note `o` will be copied over a pre-allocated `fc`
446 function visitQueryConditions (o, fc) {
447 // Copy o.args over to fc.args, running `visitQueryConditions` for each item,
448 // and checking that `and`,`or` and `each` have at least 2 conditions (if not,
449 // the first and only arg will become the item)
450 function goThroughArgs () {
451 o.args.forEach(function (queryCondition) {
452 // Make space for the new condition which will (hopefully) get pushed
453 var newQueryCondition = {}
454
455 // Get the result from the query conditions
456 var f = visitQueryConditions(queryCondition, newQueryCondition)
457
458 // A false return means not to add the condition. The `return`
459 // will interrupt the cycle, the condition will not get pushed
460 if (f === false) return
461
462 // At this point newCondition might be a `and` or `or` with
463 // wrong number of arguments (zero or 1). In that case,
464 // it will get zapped
465
466 // If it's 'and' or 'or', check the length of what gets returned
467 if (queryCondition.type === 'and' || queryCondition.type === 'or' || queryCondition.type === 'each') {
468 // newCondition is empty: do not add anything to fc
469 // This can happen if ifDefined weren't satisfied
470 if (newQueryCondition.args.length === 0) {
471 return
472
473 // Only one condition returned: get rid of logical operator, add the straight condition
474 } else if (newQueryCondition.args.length === 1) {
475 var actualQueryCondition = {}
476 var $qc = newQueryCondition.args[ 0 ]
477 var $toPush = {}
478 for (var kk in $qc) $toPush[ kk ] = $qc[ kk ]
479 fc.args.push($toPush)
480 // fc.args.push( { type: actualQueryCondition.type, args: actualQueryCondition.args } );
481
482 // Multiple queryConditions returned: the logical operator makes sense
483 } else {
484 fc.args.push(newQueryCondition)
485 }
486
487 // If it's a leaf
488 } else {
489 fc.args.push(newQueryCondition)
490 }
491 })
492 }
493
494 // Check o.ifDefined. If the corresponding element in conditionsHash
495 // is not defined, won't go there
496 // Note: ifDefined can be a list of comma-separated fields
497 if (o.ifDefined && typeof (o.ifDefined) === 'string') {
498 if (!o.ifDefined.split(',').every(function (s) { return typeof conditionsHash[ s ] !== 'undefined' })) {
499 return false
500 }
501 }
502
503 // Check o.ifNotDefined. If the corresponding element in conditionsHash
504 // is not defined, won't go there
505 // Note: ifNotDefined can be a list of comma-separated fields
506 if (o.ifNotDefined && typeof (o.ifNotDefined) === 'string') {
507 if (!o.ifNotDefined.split(',').every(function (s) { return typeof conditionsHash[ s ] === 'undefined' })) {
508 return false
509 }
510 }
511
512 // Check o.if If the corresponding element in conditionsHash
513 // is not defined, won't go there
514 // Note: ifDefined can be a list of comma-separated fields
515 if (o.if && typeof (o.if) === 'function') {
516 if (!o.if.call(self, request)) return false
517 }
518
519 // If it's `and` or `or`, will go through o.args one after the other
520 // and (possibly) add them to fc's args
521 if (o.type === 'and' || o.type === 'or') {
522 for (var kk in o) {
523 if (kk != 'args' && kk != 'type') { fc[ kk ] = o[ kk ] }
524 }
525 fc.type = o.type
526 fc.args = []
527
528 goThroughArgs()
529
530 // If it' `each`, it will still go through `o.args` but N times,
531 // once for each word found in value (and split with the separator)
532 } else if (o.type === 'each') {
533 for (var kk in o) {
534 if (kk != 'args' && kk != 'type') fc[ kk ] = o[ kk ]
535 }
536 fc.type = o.type
537 fc.args = []
538
539 // Link type defaults to "and"
540 fc.type = o.linkType || 'and'
541
542 // If the field is not in the conditionsHash, there is no
543 // point in doing this
544 if (!conditionsHash[ o.value ]) return
545
546 // Separator defaults to ' '
547 allValues = conditionsHash[ o.value ].split(o.separator || ' ')
548
549 allValues.forEach(function (value) {
550 // Assign the right value to conditionsHash and allowedFields so that
551 // referencing to #something# will work
552
553 // "as" defaults to value+'Each'
554 var k = o.as || o.value + 'Each'
555 conditionsHash[ k ] = value
556 allowedFields[ k ] = true
557
558 goThroughArgs()
559 })
560
561 // It's not `and`, `or` nor `each`: it will NOT go through its
562 // args recursively, since it's an end point. However,
563 // the second argument might be resolved by conditionsHash
564 // if it's in the right #format#
565 } else {
566 var arg0 = o.args[ 0 ]
567 var arg1 = o.args[ 1 ]
568
569 // No arg1: most likely a unary operator, let it live.
570 if (typeof (arg1) === 'undefined') {
571 fc.type = o.type
572 fc.args = []
573
574 fc.args[ 0 ] = arg0
575
576 // Two arguments. The second one must be resolved if it's
577 // in the right #format#
578 } else {
579 var sourceArgs = Array.isArray(arg1) ? arg1 : [ arg1 ]
580 var resultArgs = []
581
582 for (var i = 0, l = sourceArgs.length; i < l; i++) {
583 var arg = sourceArgs[ i ]
584 var m = (arg.match && arg.match(/^#(.*?)#$/))
585 if (m) {
586 var osf = m[ 1 ]
587
588 // If it's in form #something#, then entry MUST be in allowedFields
589 if (!allowedFields[ osf ]) throw new Error('Searched for ' + arg + ", but didn't find corresponding entry in onlineSearchSchema")
590
591 if (typeof conditionsHash[ osf ] !== 'undefined') {
592 resultArgs.push(conditionsHash[ osf ])
593 } else {
594 // It will get dropped since the required variable is not set
595 return false
596 }
597
598 // The second argument is not in form #something#: it means it's a STRAIGHT value
599 } else {
600 resultArgs.push(arg)
601 }
602 }
603
604 fc.type = o.type
605 fc.args = [ arg0, Array.isArray(arg1) ? resultArgs : resultArgs[ 0 ] ]
606 }
607 }
608 }
609
610 // Function starts here
611 var res = {}
612 visitQueryConditions(queryConditions, res)
613
614 // visitQueryConditions does a great job avoiding duplication, but
615 // top-level duplication needs to be checked here
616 if ((res.type === 'and' || res.type === 'or')) {
617 if (res.args.length === 0) return {}
618 if (res.args.length === 1) return res.args[ 0 ]
619 }
620 // console.log("RESULT:", require('util').inspect(res, { depth: 10 } ) );
621 return res
622 },
623
624 _extrapolateDocProxyAndprepareBeforeSendProxyAll: function (request, method, fullDocs, cb) {
625 var self = this
626
627 var changeFunctions = []
628 var docs = [], preparedDocs = []
629
630 async.eachSeries(
631 fullDocs,
632 function (fullDoc, callback) {
633 self.extrapolateDocProxy(request, method, fullDoc, function (err, doc) {
634 if (err) return callback(err)
635
636 docs.push(doc)
637
638 self.prepareBeforeSendProxy(request, method, doc, function (err, preparedDoc) {
639 if (err) return callback(err)
640
641 preparedDocs.push(preparedDoc)
642
643 callback(null)
644 })
645 })
646 },
647 function (err) {
648 if (err) return cb(err)
649
650 cb(null, docs, preparedDocs)
651 }
652 )
653 },
654
655 _lastParamId: function () {
656 return this.paramIds[ this.paramIds.length - 1 ]
657 },
658
659 // Check that paramsId are actually legal IDs using
660 // paramsSchema.
661 _checkParamIds: function (request, skipIdProperty, next) {
662 var self = this
663 var errors = []
664
665 // Params is empty: nothing to do, optimise a little
666 if (request.params.length === 0) return cb(null)
667
668 // Check that ALL paramIds do belong to the schema
669 self.paramIds.forEach(function (k) {
670 if (typeof (self.schema.structure[ k ]) === 'undefined') {
671 throw new Error('This paramId must be in schema: ' + k)
672 }
673 })
674
675 // If it's a remote request, check that _all_ paramIds are in params
676 // (Local API requests can avoid passing paramIds)
677 if (request.remote) {
678 self.paramIds.forEach(function (k) {
679 // "continue" if id property is to be skipped
680 if (skipIdProperty && k == self.idProperty) return
681
682 // Required paramId not there: puke!
683 if (typeof (request.params[ k ]) === 'undefined') {
684 errors.push({ field: k, message: 'Field required in the URL: ' + k })
685 }
686 })
687 // If one of the key fields was missing, puke back
688 if (errors.length) return next(new self.BadRequestError({ errors: errors }))
689 };
690
691 // Prepare skipParams and skipCast, depending on skipIdProperty
692 var skipParams = {}
693 var skipCast = [ ]
694 if (skipIdProperty) {
695 skipParams[ self.idProperty ] = [ 'required' ]
696 skipCast.push(self.idProperty)
697 }
698
699 // Validate request.params
700 self.schema.validate(request.params, { onlyObjectValues: true, skipParams: skipParams, skipCast: skipCast }, function (err, params, errors) {
701 if (err) {
702 next(err)
703 } else {
704 // There was a problem: return the errors
705 if (errors.length) {
706 next(new self.BadRequestError({ errors: errors }))
707 } else {
708 // Make sure request.params contains cast values
709 request.params = params
710
711 next(null)
712 }
713 }
714 })
715 },
716
717 _sendError: function (request, method, next, error) {
718 var self = this
719
720 // It's a local call: simply call the callback passed by the caller
721 if (!request.remote) {
722 next(error, null)
723 return
724 }
725
726 // This will happen when _sendError is passed an error straight from a callback
727 // The idea is that jsonreststores _always_ throws an HTTP error of some sort.
728
729 switch (self.chainErrors) {
730
731 case 'all':
732 next(error)
733 break
734
735 case 'none':
736 case 'nonhttp':
737
738 // CASE #1: It's not an HTTP error and it's meant to chain non-HTTP errors: chain (call next)
739 if (typeof (e[ error.name ]) === 'undefined' && self.chainErrors === 'nonhttp') {
740 next(error)
741
742 // CASE :2: Any other case. It might be an HTTP error or a JS error. Needs to handle both cases
743 } else {
744 // It's not an HTTP error: make up a new one, and incapsulate original error in it
745 if (typeof (e[ error.name ]) === 'undefined') {
746 error = new self.ServiceUnavailableError({ originalErr: error })
747 error.stack = error.originalErr.stack
748 }
749
750 // Make up the response body based on the error, attach it to the error itself
751 error.formattedErrorResponse = self.formatErrorResponse(error)
752 error.originalMethod = method
753
754 // Send the response, with `error` as pseudo-method
755 self.sendData(request, 'error', error)
756 }
757 break
758
759 }
760
761 self.logError(error)
762 },
763
764 _checkPermissionsProxy: function (request, method, cb) {
765 // It's an API request: permissions are totally skipped
766 if (!request.remote) return cb(null, true)
767
768 this.checkPermissions(request, method, cb)
769 },
770
771 _enrichBodyWithParamIdsIfRemote: function (request) {
772 var self = this
773
774 if (request.remote) {
775 self.paramIds.forEach(function (paramId) {
776 if (typeof (request.params[ paramId ]) !== 'undefined') {
777 request.body[ paramId ] = request.params[ paramId ]
778 }
779 })
780 }
781 },
782
783 errorInSending: function (request, method, data, when, error) {
784 self.logError(error)
785 },
786
787 // Method that will call the correct `protocolSend?????` method depending on
788 // request.protocol
789 // To keep the signature short, the status will be worked out depending on
790 // what's being sent
791 sendData: function (request, method, data) {
792 var n = 'protocolSend' + request.protocol
793 var f = this[ n ]
794
795 var self = this
796
797 // Sets status and responseBody
798 var status = 200
799 switch (method) {
800 case 'post': status = 201; break
801 case 'put': if (request.putNew) status = 201; break
802 case 'delete': if (data == '') status = 204; break
803 case 'error': status = data.httpError; break
804 };
805
806 // The method must be implemented
807 if (!f) throw ('Error: function self.' + n + ' not implemented!')
808
809 // Call the `internalBeforeSendData()` hook
810 self._internalBeforeSendData(request, method, data, function (err) {
811 if (err) return self.errorInSending(request, method, data, 'before', err)
812
813 // Call the function that _actually_ sends data
814 f.call(self, request, method, data, status, function (err) {
815 if (err) return self.errorInSending(request, method, data, 'during', err)
816
817 // Call the `internalAfterSendData()` hook
818 self._internalAfterSendData(request, method, data, function (err) {
819 if (err) return self.errorInSending(request, method, data, 'after', err)
820
821 // No-op. This is the end of the call chain.
822 })
823 })
824 })
825 },
826
827 _internalBeforeSendData: function (request, method, data, cb) {
828 cb(null)
829 },
830
831 _internalAfterSendData: function (request, method, data, cb) {
832 cb(null)
833 },
834
835 protocolListen: function (protocol, params) {
836 var n = 'protocolListen' + protocol
837 var f = this[ n ]
838
839 // The method must be implemented
840 if (!f) throw ('Error: function self.' + n + ' not implemented!')
841
842 f.call(this, params)
843 },
844
845 // Check if there is already a record where field `field`
846 // already has value `value` and it's not id `id`
847 _isFieldUnique: function (field, value, id, cb) {
848 var self = this
849
850 var conditions
851
852 if (id) {
853 conditions = {
854 type: 'and',
855 args: [
856 {
857 type: 'eq',
858 args: [ field, value ]
859 },
860 {
861 type: 'ne',
862 args: [ self.idProperty, id ]
863 }
864 ]
865 }
866 } else {
867 conditions = {
868 type: 'eq',
869 args: [ field, value ]
870 }
871 };
872
873 self.dbLayer.select(conditions, function (err, data, total) {
874 if (err) return cb(err)
875
876 if (total) return cb(null, false)
877 cb(null, true)
878 })
879 },
880
881 _areFieldsUnique: function (id, body, cb) {
882 var self = this
883
884 var errors = []
885
886 async.each(
887 Object.keys(self._uniqueFields),
888 function (field, cb) {
889 // The field is not defined
890 if (typeof body[ field ] === 'undefined' || body[ field ] == '') return cb(null)
891
892 self._isFieldUnique(field, body[ field ], id, function (err, isUnique) {
893 if (err) return cb(err)
894
895 // If it's a duplicate, enrich the `errors` array
896 if (!isUnique) errors.push({ field: field, message: (self.schema.structure[field].uniqueMessage || 'Field already in database') })
897
898 cb(null)
899 })
900 },
901 function (err) {
902 if (err) return cb(err)
903
904 // There are no errors: all good, return "true"
905 if (errors.length == 0) return cb(null, true, [])
906
907 // Errors: return them, along with `false`
908 return cb(null, false, errors)
909 }
910 )
911 },
912
913 _makePost: function (request, next) {
914 var self = this
915
916 if (typeof (next) !== 'function') next = function () {}
917
918 // Prepare request.data
919 request.data = request.data || {}
920
921 // Check that the method is implemented
922 if (!self.handlePost && request.remote) {
923 return self._sendError(request, 'post', next, new self.NotImplementedError())
924 }
925
926 // Check the IDs
927 self._checkParamIds(request, true, function (err) {
928 if (err) return self._sendError(request, 'post', next, err)
929
930 var protectedFields = []
931 Object.keys(self.schema.structure).forEach((field) => {
932 if (self.schema.structure[ field ].protected) protectedFields.push(field)
933 })
934
935 // Protected field are not allowed here
936 // (Except the ones marked in `bodyComputed`)
937 protectedFields.forEach((field) => {
938 if (typeof (request.body[ field ]) !== 'undefined') {
939 // NOTE: Will only delete it if it wasn't marked as "computed" in the request.
940 if (typeof (request.bodyComputed) === 'object' && request.bodyComputed != null && !request.bodyComputed[ field ]) {
941 delete request.body[ field ]
942 }
943 }
944 })
945
946 self._doAutoLookup(request, 'post', function (err) {
947 if (err) return self._sendError(request, 'post', next, err)
948
949 self.prepareBody(request, 'post', request.body, function (err, preparedBody) {
950 if (err) return self._sendError(request, 'post', next, err)
951
952 // Request is changed, old value is saved
953 request.bodyBeforePrepare = request.body
954 request.body = preparedBody
955
956 var skipParamsObject = {}
957 skipParamsObject[ self.idProperty ] = [ 'required' ]
958 self._enrichBodyWithParamIdsIfRemote(request)
959
960 // Delete _children which mustn't be here regardless
961 delete request.body._children
962
963 // Make up a hash of CHANGED body fields
964 // WHY would protected fields be defined?
965 // BECAUSE prepareBody might have done it, or a field might be bodyComputed (effectively exceptions to early deletion)
966 var changedBodyFields = {}
967 protectedFields.forEach((field) => {
968 if (typeof (request.body[ field ]) !== 'undefined') {
969 changedBodyFields[ field ] = true
970 }
971 })
972
973 self.schema.validate(request.body, { skipParams: skipParamsObject, skipCast: [ self.idProperty ] }, function (err, validatedBody, errors) {
974 if (err) return self._sendError(request, 'post', next, err)
975
976 // Validation might have set some defaults on protected fields.
977 // Unless they were marked as changed, DELETE those.
978 protectedFields.forEach((field) => {
979 if (!changedBodyFields[ field ]) {
980 delete validatedBody[ field ]
981 }
982 })
983
984 request.bodyBeforeValidation = request.body
985 request.body = validatedBody
986
987 if (errors.length) return self._sendError(request, 'post', next, new self.UnprocessableEntityError({ errors: errors }))
988 self.afterValidate(request, 'post', function (err) {
989 if (err) return self._sendError(request, 'post', next, err)
990
991 self._areFieldsUnique(null, request.body, function (err, allUnique, errors) {
992 if (err) return self._sendError(request, 'post', next, err)
993
994 if (!allUnique) return self._sendError(request, 'post', next, new self.UnprocessableEntityError({ errors: errors }))
995
996 // Actually check permissions
997 self._checkPermissionsProxy(request, 'post', function (err, granted, message) {
998 if (err) return self._sendError(request, 'post', next, err)
999
1000 if (!granted) return self._sendError(request, 'post', next, new self.ForbiddenError(message))
1001
1002 self.afterCheckPermissions(request, 'post', function (err) {
1003 if (err) return self._sendError(request, 'post', next, err)
1004
1005 // Clean up body from things that are not to be submitted
1006 self.schema.cleanup(request.body, 'doNotSave')
1007
1008 self.schema.makeId(request.body, function (err, forceId) {
1009 if (err) return self._sendError(request, 'post', next, err)
1010
1011 self.implementInsert(request, forceId, function (err, fullDoc) {
1012 if (err) return self._sendError(request, 'post', next, err)
1013
1014 request.data.fullDoc = fullDoc
1015
1016 self._repositionBasedOnOptions(request.data.fullDoc, request.options.putBefore, request.options.putDefaultPosition, false, function (err) {
1017 if (err) return self._sendError(request, 'post', next, err)
1018
1019 self.afterDbOperation(request, 'post', function (err) {
1020 if (err) return self._sendError(request, 'post', next, err)
1021
1022 self.extrapolateDocProxy(request, 'post', request.data.fullDoc, function (err, doc) {
1023 if (err) return self._sendError(request, 'post', next, err)
1024
1025 request.data.doc = doc
1026
1027 self.prepareBeforeSendProxy(request, 'post', request.data.doc, function (err, preparedDoc) {
1028 if (err) return self._sendError(request, 'post', next, err)
1029
1030 request.data.preparedDoc = preparedDoc
1031
1032 self.afterEverything(request, 'post', function (err) {
1033 if (err) return self._sendError(request, 'post', next, err)
1034
1035 if (request.remote) {
1036 if (self.echoAfterPost) {
1037 self.sendData(request, 'post', request.data.preparedDoc)
1038 } else {
1039 self.sendData(request, 'post', '')
1040 }
1041 } else {
1042 next(null, request.data.preparedDoc, request)
1043 }
1044 })
1045 })
1046 })
1047 })
1048 })
1049 })
1050 })
1051 })
1052 })
1053 })
1054 })
1055 })
1056 })
1057 })
1058 })
1059 },
1060
1061 _makePut: function (request, next) {
1062 var self = this
1063 var overwrite
1064
1065 if (typeof (next) !== 'function') next = function () {}
1066
1067 // Prepare request.data
1068 request.data = request.data || {}
1069
1070 if (!self.handlePut && !request.options.field && request.remote) {
1071 return self._sendError(request, 'put', next, new self.NotImplementedError())
1072 }
1073
1074 // DETOUR: It's a reposition. Not allowed here!
1075 if (typeof (request.options.putBefore) !== 'undefined') {
1076 return cb(new Error('Option putBefore not allowed in OneFieldStore'))
1077 }
1078
1079 // Check the IDs.
1080 self._checkParamIds(request, false, function (err) {
1081 if (err) return self._sendError(request, 'put', next, err)
1082
1083 var protectedFields = []
1084 Object.keys(self.schema.structure).forEach((field) => {
1085 if (self.schema.structure[ field ].protected) protectedFields.push(field)
1086 })
1087
1088 // Protected field are not allowed here
1089 // (Except the ones marked in `bodyComputed`)
1090 protectedFields.forEach((field) => {
1091 if (typeof (request.body[ field ]) !== 'undefined') {
1092 // NOTE: Will only delete it if it wasn't marked as "computed" in the request.
1093 if (typeof (request.bodyComputed) === 'object' && request.bodyComputed != null && !request.bodyComputed[ field ]) {
1094 delete request.body[ field ]
1095 }
1096 }
1097 })
1098
1099 self._doAutoLookup(request, 'put', function (err) {
1100 if (err) return self._sendError(request, 'post', next, err)
1101
1102 self.prepareBody(request, 'put', request.body, function (err, preparedBody) {
1103 if (err) return self._sendError(request, 'put', next, err)
1104
1105 // Request is changed, old value is saved
1106 request.bodyBeforePrepare = request.body
1107 request.body = preparedBody
1108
1109 self._enrichBodyWithParamIdsIfRemote(request)
1110
1111 // Delete _children which mustn't be here regardless
1112 delete request.body._children
1113
1114 if (request.options.field) {
1115 var errorsInPiggyField = []
1116
1117 // Only the single field is allowed in body (and the paramId fields)
1118 for (var field in request.body) {
1119 if (self.paramIds.indexOf(field) == -1 && field != request.options.field) {
1120 errorsInPiggyField.push({ field: field, message: 'Field not allowed because not a paramId nor the single field: ' + field + ' in ' + self.storeName })
1121 }
1122 }
1123
1124 // If it's a single field, then the single field's value MUST be set in body
1125 if (typeof request.body[ request.options.field ] === 'undefined') {
1126 errorsInPiggyField.push({ field: request.options.field, message: 'When putting onto a field, that field must be in the payload' })
1127 }
1128
1129 // If there was an error, then quit it
1130 if (errorsInPiggyField.length) return self._sendError(request, 'put', next, new self.UnprocessableEntityError({ errors: errorsInPiggyField }))
1131 }
1132
1133 // Make up a hash of CHANGED body fields
1134 // WHY would protected fields be defined?
1135 // BECAUSE prepareBody might have done it, or a field might be bodyComputed (effectively exceptions to early deletion)
1136 var changedBodyFields = {}
1137 protectedFields.forEach((field) => {
1138 if (typeof (request.body[ field ]) !== 'undefined') {
1139 changedBodyFields[ field ] = true
1140 }
1141 })
1142
1143 self.schema.validate(request.body, { onlyObjectValues: !!request.options.field }, function (err, validatedBody, errors) {
1144 if (err) return self._sendError(request, 'put', next, err)
1145
1146 // Validation might have set some defaults on protected fields.
1147 // Unless they were marked as changed, DELETE those.
1148 protectedFields.forEach((field) => {
1149 if (!changedBodyFields[ field ]) {
1150 delete validatedBody[ field ]
1151 }
1152 })
1153
1154 request.bodyBeforeValidation = request.body
1155 request.body = validatedBody
1156
1157 if (errors.length) return self._sendError(request, 'put', next, new self.UnprocessableEntityError({ errors: errors }))
1158
1159 self.afterValidate(request, 'put', function (err) {
1160 if (err) return self._sendError(request, 'put', next, err)
1161
1162 // Fetch the doc
1163 self.implementFetchOne(request, function (err, fullDoc) {
1164 if (err) return self._sendError(request, 'put', next, err)
1165
1166 // OneFieldStores will only ever work on already existing records
1167 if (!fullDoc && request.options.field) {
1168 return self._sendError(request, 'put', next, new self.NotFoundError())
1169 }
1170
1171 // Check the 'overwrite' option
1172 if (typeof (request.options.overwrite) !== 'undefined') {
1173 if (fullDoc && !request.options.overwrite) {
1174 self._sendError(request, 'put', next, new self.PreconditionFailedError())
1175 } else if (!fullDoc && request.options.overwrite) {
1176 self._sendError(request, 'put', next, new self.PreconditionFailedError())
1177 } else {
1178 continueAfterFetch()
1179 }
1180 } else {
1181 continueAfterFetch()
1182 }
1183
1184 function continueAfterFetch () {
1185 // It's a NEW doc: it will need to be an insert, _and_ permissions will be
1186 // done on inputted data
1187 if (!fullDoc) {
1188 request.putNew = true
1189
1190 self._areFieldsUnique(null, request.body, function (err, allUnique, errors) {
1191 if (err) return self._sendError(request, 'post', next, err)
1192
1193 if (!allUnique) return self._sendError(request, 'post', next, new self.UnprocessableEntityError({ errors: errors }))
1194
1195 // Actually check permissions
1196 self._checkPermissionsProxy(request, 'put', function (err, granted, message) {
1197 if (err) return self._sendError(request, 'put', next, err)
1198
1199 if (!granted) return self._sendError(request, 'put', next, new self.ForbiddenError(message))
1200
1201 self.afterCheckPermissions(request, 'put', function (err) {
1202 if (err) return self._sendError(request, 'put', next, err)
1203
1204 // Clean up body from things that are not to be submitted
1205 // if( self.schema ) self.schema.cleanup( body, 'doNotSave' );
1206 self.schema.cleanup(request.body, 'doNotSave')
1207
1208 // Since it's a new record, if there were any defaults from the
1209 // previous validation, assign it.
1210 // But ONLY if the field hasn't already been assigned by another hook before
1211
1212 self.implementInsert(request, null, function (err, fullDoc) {
1213 if (err) return self._sendError(request, 'put', next, err)
1214
1215 request.data.fullDoc = fullDoc
1216
1217 self._repositionBasedOnOptions(request.data.fullDoc, request.options.putBefore, request.options.putDefaultPosition, false, function (err) {
1218 if (err) return self._sendError(request, 'put', next, err)
1219
1220 self.afterDbOperation(request, 'put', function (err) {
1221 if (err) return self._sendError(request, 'put', next, err)
1222
1223 self.extrapolateDocProxy(request, 'put', request.data.fullDoc, function (err, doc) {
1224 if (err) return self._sendError(request, 'put', next, err)
1225
1226 request.data.doc = doc
1227
1228 self.prepareBeforeSendProxy(request, 'put', request.data.doc, function (err, preparedDoc) {
1229 if (err) return self._sendError(request, 'put', next, err)
1230
1231 request.data.preparedDoc = preparedDoc
1232
1233 self.afterEverything(request, 'put', function (err) {
1234 if (err) return self._sendError(request, 'put', next, err)
1235
1236 if (request.remote) {
1237 if (self.echoAfterPut) {
1238 self.sendData(request, 'put', request.data.preparedDoc)
1239 } else {
1240 self.sendData(request, 'put', '')
1241 }
1242 } else {
1243 next(null, request.data.preparedDoc, request)
1244 }
1245 })
1246 })
1247 })
1248 })
1249 })
1250 })
1251 })
1252 })
1253 })
1254
1255 // It's an EXISTING doc: it will need to be an update, _and_ permissions will be
1256 // done on inputted data AND existing doc
1257 } else {
1258 request.data.fullDoc = fullDoc
1259 request.putExisting = true
1260
1261 self._areFieldsUnique(fullDoc[ self.idProperty ], request.body, function (err, allUnique, errors) {
1262 if (err) return self._sendError(request, 'post', next, err)
1263
1264 if (!allUnique) return self._sendError(request, 'post', next, new self.UnprocessableEntityError({ errors: errors }))
1265
1266 self.extrapolateDocProxy(request, 'put', request.data.fullDoc, function (err, doc) {
1267 if (err) return self._sendError(request, 'put', next, err)
1268
1269 request.data.doc = doc
1270
1271 // Actually check permissions
1272 self._checkPermissionsProxy(request, 'put', function (err, granted, message) {
1273 if (err) return self._sendError(request, 'put', next, err)
1274
1275 if (!granted) return self._sendError(request, 'put', next, new self.ForbiddenError(message))
1276
1277 self.afterCheckPermissions(request, 'put', function (err) {
1278 if (err) return self._sendError(request, 'put', next, err)
1279
1280 // Clean up body from things that are not to be submitted
1281 // if( self.schema ) self.schema.cleanup( body, 'doNotSave' );
1282 self.schema.cleanup(request.body, 'doNotSave')
1283
1284 // Since it's a existing record, if body isn't assigned it and existing record has a value,
1285 // assign the existing value
1286 protectedFields.forEach((field) => {
1287 if (typeof (request.body[ field ]) === 'undefined' && typeof (request.data.doc[ field ]) !== 'undefined') {
1288 request.body[ field ] = request.data.doc[ field ]
1289 }
1290 })
1291
1292 self.implementUpdate(request, !request.options.field, function (err, fullDocAfter) {
1293 if (err) return self._sendError(request, 'put', next, err)
1294
1295 // Update must have worked -- if it hasn't, there was a (bad) problem
1296 if (!fullDocAfter) return self._sendError(request, 'put', next, new Error('Error re-fetching document after update in put'))
1297
1298 request.data.fullDocAfter = fullDocAfter
1299
1300 self._repositionBasedOnOptions(request.data.fullDoc, request.options.putBefore, request.options.putDefaultPosition, true, function (err) {
1301 if (err) return self._sendError(request, 'put', next, err)
1302
1303 self.afterDbOperation(request, 'put', function (err) {
1304 if (err) return self._sendError(request, 'put', next, err)
1305
1306 self.extrapolateDocProxy(request, 'put', request.data.fullDocAfter, function (err, docAfter) {
1307 if (err) return self._sendError(request, 'put', next, err)
1308
1309 request.data.docAfter = docAfter
1310
1311 self.prepareBeforeSendProxy(request, 'put', request.data.docAfter, function (err, preparedDoc) {
1312 if (err) return self._sendError(request, 'put', next, err)
1313
1314 request.data.preparedDoc = preparedDoc
1315
1316 self.afterEverything(request, 'put', function (err) {
1317 if (err) return self._sendError(request, 'put', next, err)
1318
1319 // Manipulate fullDoc: at this point, it's the WHOLE database record,
1320 // whereas I only want returned paramIds AND the piggyField
1321 // if( request.options.field ){
1322 // for( var field in fullDocAfter ){
1323 // if( self.paramIds.indexOf( field ) == -1 && field != request.options.field ) delete fullDocAfter[ field ];
1324 // }
1325 // }
1326
1327 if (request.remote) {
1328 if (self.echoAfterPut) {
1329 self.sendData(request, 'put', request.data.preparedDoc)
1330 } else {
1331 self.sendData(request, 'put', '')
1332 }
1333 } else {
1334 next(null, request.data.preparedDoc, request)
1335 }
1336 })
1337 })
1338 })
1339 })
1340 })
1341 })
1342 })
1343 })
1344 })
1345 })
1346 } // Existing or new doc
1347 } // continueAfterFetch
1348 })
1349 })
1350 })
1351 })
1352 })
1353 })
1354 },
1355
1356 _makeGetQuery: function (request, next) {
1357 var self = this
1358
1359 if (typeof (next) !== 'function') next = function () {}
1360
1361 // Prepare request.data
1362 request.data = request.data || {}
1363
1364 // Check that the method is implemented
1365 if (!self.handleGetQuery && !request.options.field && request.remote) {
1366 return self._sendError(request, 'getQuery', next, new self.NotImplementedError())
1367 }
1368
1369 // Check the IDs. If there is a problem, it means an ID is broken:
1370 // return a BadRequestError
1371 self._checkParamIds(request, true, function (err) {
1372 if (err) return self._sendError(request, 'getQuery', next, err)
1373
1374 self._doAutoLookup(request, 'getQuery', function (err) {
1375 if (err) return self._sendError(request, 'post', next, err)
1376
1377 self._checkPermissionsProxy(request, 'getQuery', function (err, granted, message) {
1378 if (err) return self._sendError(request, 'getQuery', next, err)
1379
1380 if (!granted) return self._sendError(request, 'getQuery', next, new self.ForbiddenError(message))
1381
1382 self.afterCheckPermissions(request, 'getQuery', function (err) {
1383 if (err) return self._sendError(request, 'getQuery', next, err)
1384
1385 self.onlineSearchSchema.validate(request.options.conditionsHash, { onlyObjectValues: true }, function (err, conditionsHash, errors) {
1386 if (err) return self._sendError(request, 'getQuery', next, err)
1387
1388 // Errors in casting: give up, run away
1389 if (errors.length) return self._sendError(request, 'getQuery', next, new self.BadRequestError({ errors: errors }))
1390
1391 // Actually assigning cast and validated conditions to `options`
1392 request.options.conditionsHash = conditionsHash
1393
1394 self.afterValidate(request, 'getQuery', function (err) {
1395 if (err) return self._sendError(request, 'getQuery', next, err)
1396
1397 var inn = function (o) { return require('util').inspect(o, { depth: 10 }) }
1398 // console.log("CONDITION HASH:", inn( conditionsHash ) );
1399 // console.log("QUERY CONDITIONS:", inn( self.queryConditions ) );
1400
1401 // Resolve queryConditions (with all #variable# replacement, `each` etc.
1402 // properly expanded and ready to be fed to `implementQuery`)
1403 request.options.resolvedQueryConditions = self._resolveQueryConditions(
1404 self.queryConditions,
1405 request.options.conditionsHash,
1406 self.onlineSearchSchema.structure,
1407 request
1408 )
1409 // console.log("RESOLVED QUERY CONDITIONS:", inn( request.options.resolvedQueryConditions ) );
1410
1411 // TODO: Document if it sticks
1412 self.manipulateQueryConditions(request, 'getQuery', function (err) {
1413 if (err) return self._sendError(request, 'getQuery', next, err)
1414
1415 self.implementQuery(request, function (err, fullDocs, total, grandTotal) {
1416 if (err) return self._sendError(request, 'getQuery', next, err)
1417
1418 // Make `total` and `grandTotal` part of the request, exposing them to all callbacks
1419 request.data.fullDocs = fullDocs
1420 request.data.total = total
1421 request.data.grandTotal = grandTotal
1422
1423 self.afterDbOperation(request, 'getQuery', function (err) {
1424 if (err) return self._sendError(request, 'getQuery', next, err)
1425
1426 self._extrapolateDocProxyAndprepareBeforeSendProxyAll(request, 'getQuery', request.data.fullDocs, function (err, docs, preparedDocs) {
1427 if (err) return self._sendError(request, 'getQuery', next, err)
1428
1429 request.data.docs = docs
1430 request.data.preparedDocs = preparedDocs
1431
1432 self.afterEverything(request, 'getQuery', function (err) {
1433 if (err) return self._sendError(request, 'getQuery', next, err)
1434
1435 // Remote request: set headers, and send the doc back (if echo is on)
1436 if (request.remote) {
1437 self.sendData(request, 'getQuery', request.data.preparedDocs)
1438 // Local request: simply return the doc to the asking function
1439 } else {
1440 next(null, request.data.preparedDocs, request)
1441 }
1442 })
1443 })
1444 })
1445 })
1446 })
1447 })
1448 })
1449 })
1450 })
1451 })
1452 })
1453 },
1454
1455 _makeGet: function (request, next) {
1456 var self = this
1457
1458 if (typeof (next) !== 'function') next = function () {}
1459
1460 // Prepare request.data
1461 request.data = request.data || {}
1462
1463 // Check that the method is implemented
1464 if (!self.handleGet && request.remote) {
1465 return self._sendError(request, 'get', next, new self.NotImplementedError())
1466 }
1467
1468 // Check the IDs
1469 self._checkParamIds(request, false, function (err) {
1470 if (err) return self._sendError(request, 'get', next, err)
1471
1472 self._doAutoLookup(request, 'get', function (err) {
1473 if (err) return self._sendError(request, 'post', next, err)
1474
1475 // Fetch the doc.
1476 self.implementFetchOne(request, function (err, fullDoc) {
1477 if (err) return self._sendError(request, 'get', next, err)
1478
1479 if (!fullDoc) return self._sendError(request, 'get', next, new self.NotFoundError())
1480
1481 request.data.fullDoc = fullDoc
1482
1483 self.afterDbOperation(request, 'get', function (err) {
1484 if (err) return self._sendError(request, 'get', next, err)
1485
1486 self.extrapolateDocProxy(request, 'get', fullDoc, function (err, doc) {
1487 if (err) return self._sendError(request, 'get', next, err)
1488
1489 request.data.doc = doc
1490
1491 // Check the permissions
1492 self._checkPermissionsProxy(request, 'get', function (err, granted, message) {
1493 if (err) return self._sendError(request, 'get', next, err)
1494
1495 if (!granted) return self._sendError(request, 'get', next, new self.ForbiddenError(message))
1496
1497 self.afterCheckPermissions(request, 'get', function (err) {
1498 if (err) return self._sendError(request, 'get', next, err)
1499
1500 // "preparing" the doc. The same function is used by GET for collections
1501 self.prepareBeforeSendProxy(request, 'get', doc, function (err, preparedDoc) {
1502 if (err) return self._sendError(request, 'get', next, err)
1503
1504 request.data.preparedDoc = preparedDoc
1505
1506 // Just in case: clean up any field that returned from the schema, and shouldn't have been
1507 // there in the first place
1508 self.schema.cleanup(preparedDoc, 'doNotSave')
1509
1510 self.afterEverything(request, 'get', function (err) {
1511 if (err) return self._sendError(request, 'get', next, err)
1512
1513 // Manipulate preparedDoc: at this point, it's the WHOLE database record,
1514 // whereas I only want returned paramIds AND the piggyField
1515 // if( request.options.field ){
1516 // for( var field in preparedDoc ){
1517 // if( ! self.paramIds[ field ] && field != request.options.field ) delete preparedDoc[ field ];
1518 // }
1519 // }
1520
1521 // Remote request: set headers, and send the doc back
1522 if (request.remote) {
1523 // Send "prepared" doc
1524 self.sendData(request, 'get', preparedDoc)
1525
1526 // Local request: simply return the doc to the asking function
1527 } else {
1528 next(null, preparedDoc, request)
1529 }
1530 })
1531 })
1532 })
1533 })
1534 })
1535 })
1536 })
1537 })
1538 })
1539 },
1540
1541 _makeDelete: function (request, next) {
1542 var self = this
1543
1544 if (typeof (next) !== 'function') next = function () {}
1545
1546 // Prepare request.data
1547 request.data = request.data || {}
1548
1549 // Check that the method is implemented
1550 if (!self.handleDelete && request.remote) {
1551 return self._sendError(request, 'delete', next, new self.NotImplementedError())
1552 }
1553
1554 // Check the IDs
1555 self._checkParamIds(request, false, function (err) {
1556 if (err) return self._sendError(request, 'delete', next, err)
1557
1558 self._doAutoLookup(request, 'delete', function (err) {
1559 if (err) return self._sendError(request, 'post', next, err)
1560
1561 // Fetch the doc.
1562 self.implementFetchOne(request, function (err, fullDoc) {
1563 if (err) return self._sendError(request, 'delete', next, err)
1564
1565 if (!fullDoc) return self._sendError(request, 'delete', next, new self.NotFoundError())
1566
1567 request.data.fullDoc = fullDoc
1568
1569 self.extrapolateDocProxy(request, 'delete', fullDoc, function (err, doc) {
1570 if (err) return self._sendError(request, 'delete', next, err)
1571
1572 request.data.doc = doc
1573
1574 // Check the permissions
1575 self._checkPermissionsProxy(request, 'delete', function (err, granted, message) {
1576 if (err) return self._sendError(request, 'delete', next, err)
1577
1578 if (!granted) return self._sendError(request, 'delete', next, new self.ForbiddenError(message))
1579
1580 self.afterCheckPermissions(request, 'delete', function (err) {
1581 if (err) return self._sendError(request, 'delete', next, err)
1582
1583 // Actually delete the document
1584 self.implementDelete(request, function (err, deletedRecord) {
1585 if (err) return self._sendError(request, 'delete', next, err)
1586
1587 // If nothing was returned, we have a problem: the record wasn't found (it
1588 // must have disappeared between implementFetchOne() above and now)
1589 if (!deletedRecord) return self._sendError(request, 'delete', next, new Error("Error deleting a record in 'delete`: record to be deleted not found"))
1590
1591 self.afterDbOperation(request, 'delete', function (err) {
1592 if (err) return self._sendError(request, 'delete', next, err)
1593
1594 self.prepareBeforeSendProxy(request, 'delete', doc, function (err, preparedDoc) {
1595 if (err) return self._sendError(request, 'delete', next, err)
1596
1597 request.data.preparedDoc = preparedDoc
1598
1599 self.afterEverything(request, 'delete', function (err) {
1600 if (err) return self._sendError(request, 'delete', next, err)
1601
1602 if (request.remote) {
1603 if (self.echoAfterDelete) {
1604 self.sendData(request, 'delete', preparedDoc)
1605 } else {
1606 self.sendData(request, 'delete', '')
1607 }
1608 } else {
1609 next(null, doc, request)
1610 }
1611 })
1612 })
1613 })
1614 })
1615 })
1616 })
1617 })
1618 })
1619 })
1620 })
1621 },
1622
1623 apiGetQuery: function (options, next) {
1624 // Make up the request
1625 var request = new Object()
1626
1627 request.remote = false
1628 request.body = {}
1629 if (options.apiParams) request.params = options.apiParams
1630 else request.params = {}
1631
1632 request.session = options.session || {}
1633 request.options = this._co(options)
1634 request.options.delete = request.options.delete || !!this.deleteAfterGetQuery
1635
1636 // Actually run the request
1637 this._makeGetQuery(request, next)
1638 },
1639
1640 apiGet: function (id, options, next) {
1641 // Make `options` argument optional
1642 var len = arguments.length
1643
1644 if (len == 2) { next = options; options = {} };
1645
1646 var request = new Object()
1647
1648 request.remote = false
1649 request.options = options
1650 request.body = {}
1651 if (options.apiParams) request.params = options.apiParams
1652 else { request.params = {}; request.params[ this.idProperty ] = id }
1653 request.session = options.session || {}
1654
1655 // Actually run the request
1656 this._makeGet(request, next)
1657 },
1658
1659 apiPut: function (body, options, next) {
1660 // This will only work if this.idProperty is included in the body object
1661 if (typeof (body[ this.idProperty ]) === 'undefined') {
1662 throw (new Error('When calling Store.apiPut with an ID of null, id MUST be in body'))
1663 }
1664
1665 // Make `options` argument optional
1666 var len = arguments.length
1667 if (len == 2) { next = options; options = {} };
1668
1669 // Make up the request
1670 var request = new Object()
1671 request.remote = false
1672 request.options = options
1673 request.body = this._co(body)
1674 if (options.apiParams) request.params = options.apiParams
1675 else { request.params = {}; request.params[ this.idProperty ] = body[ this.idProperty ] }
1676 request.session = options.session || {}
1677
1678 delete request.body._children
1679
1680 // Actually run the request
1681 this._makePut(request, next)
1682 },
1683
1684 apiPost: function (body, options, next) {
1685 // Make `options` argument optional
1686 var len = arguments.length
1687 if (len == 2) { next = options; options = {} };
1688
1689 // Make up the request
1690 var request = new Object()
1691 request.remote = false
1692 request.options = options
1693 request.params = options.apiParams || {}
1694 request.session = options.session || {}
1695 request.body = this._co(body)
1696
1697 delete request.body._children
1698
1699 // Actually run the request
1700 this._makePost(request, next)
1701 },
1702
1703 apiDelete: function (id, options, next) {
1704 // Make `options` argument optional
1705 var len = arguments.length
1706 if (len == 2) { next = options; options = {} };
1707
1708 // Make up the request
1709 var request = new Object()
1710 request.body = {}
1711 request.options = options
1712 if (options.apiParams) request.params = options.apiParams
1713 else { request.params = {}; request.params[ this.idProperty ] = id }
1714 request.session = options.session || {}
1715
1716 // Actually run the request
1717 this._makeDelete(request, next)
1718 }
1719
1720})
1721
1722// Get store from the class' registry
1723Store.getStore = function (storeName) {
1724 return Store.registry[ storeName ]
1725}
1726
1727// Delete the store from the class' registry
1728Store.deleteStore = function (storeName) {
1729 delete Store.registry[ storeName ]
1730}
1731
1732// Get all stores as a hash
1733Store.getAllStores = function () {
1734 return Store.registry
1735}
1736
1737// Initialise all stores, running their .init() function
1738Store.init = function () {
1739 Object.keys(Store.registry).forEach(function (key) {
1740 var store = Store.registry[ key ]
1741 store.init()
1742 })
1743}
1744
1745exports = module.exports = Store
1746
1747/* Store's own "class" variables */
1748Store.artificialDelay = 0
1749Store.registry = {}
1750
1751// Embed important mixins so that they are available
1752// without an extra require (they are VERY common)
1753Store.SimpleDbLayerMixin = SimpleDbLayerMixin
1754Store.HTTPMixin = HTTPMixin
1755
1756Store.document = function (s) {
1757 // This MUST be set
1758 if (!s.getFullPublicURL) s.getFullPublicURL = Store.prototype.getFullPublicURL
1759
1760 function f (path) {
1761 var scope = this
1762 var p
1763 var key = path.split('.')[0]
1764
1765 // Step 1: find the scope DIRECTLY associated to the value
1766 // At the end of this, `p` is the right scope.
1767 // I do this because this could be in the object directly, or the proto
1768
1769 // Search for the value
1770 p = { __proto__: scope }
1771 while ((p = p.__proto__)) {
1772 if (p.hasOwnProperty(key)) break
1773 }
1774
1775 // This shouldn't really happen
1776 if (!p) {
1777 // return "[[Could not find '" + path + "' in current scope]]";
1778 return ''
1779 }
1780
1781 // Step 2: get the value from the path, and work the string a little,
1782 // take newlines out etc.
1783
1784 // Get value from path
1785 var str = DO.get(p, path)
1786 if (typeof (str) === 'undefined') str = '' // This is in case p[k] exist, but a sub-key was references
1787 if (typeof (str) !== 'string') return `[[Index ${key} exists, but ${path} is not a string, it is ${to} ]]`
1788
1789 // Fix up string
1790 str = str.replace(/^[\n\r]/, '')
1791 var spaces = str.match(/^\s*/m)
1792 var regexp = new RegExp('^' + spaces, 'gm')
1793 str = str.replace(regexp, '')
1794
1795 // Turn to markdown
1796 // str = marked( str ); // MD DELETED
1797
1798 // Step 3: Resolve {{something}} into the corresponding value in the parent's prototype
1799
1800 // Search for {{something}} in the string, and for each instance, se
1801 str = str.replace(/\{\{(.*?)\}\}/g, (match, path) => {
1802 var q = p
1803 while ((q = q.__proto__)) {
1804 if (q.hasOwnProperty(key)) return f.call(q, path)
1805 }
1806 return '[[Unable to lookup in prototype: ' + match + ']]'
1807 })
1808
1809 return str
1810 }
1811
1812 function callAll (s, method, rn) {
1813 var l = []
1814
1815 p = { __proto__: s }
1816 while ((p = p.__proto__)) {
1817 if (p.hasOwnProperty(method) && typeof (p[ method] === 'function')) l.unshift(p[ method ])
1818 }
1819 l.forEach(m => {
1820 m.call(s, s, rn)
1821 })
1822 }
1823
1824 var rn = {}
1825 var storeName = rn.storeName = s.storeName
1826
1827 if (s['main-doc']) rn['main-doc'] = f.call(s, 'main-doc')
1828
1829 // Get backend info (if backend is present)
1830 rn.backEnd = {}
1831 if (s.dbLayer) {
1832 rn.backEnd.collectionName = s.collectionName
1833 rn.backEnd.hardLimitOnQueries = s.hardLimitOnQueries
1834 }
1835
1836 // Look for derivative stores
1837 var parents = []
1838 proto = s.__proto__
1839 while ((proto = proto.__proto__)) {
1840 if (proto.hasOwnProperty('storeName') && proto.storeName) parents.push(proto.storeName)
1841 }
1842 if (parents.length) rn.parents = parents
1843
1844 if (s._singleFields && Object.keys(s._singleFields).length) rn.singleFields = s._singleFields
1845 else rn.singleFields = {}
1846
1847 rn.strictSchemaOnFetch = !!s.strictSchemaOnFetch
1848
1849 rn.nested = []
1850 if (s.nested && s.nested.length) {
1851 nested = rn.nested = []
1852
1853 s.nested.forEach(function (entry) {
1854 var line = {}
1855 line.type = entry.type
1856 var foreignStore = entry.store.storeName
1857 line.foreignStoreData = '_children.' + foreignStore
1858 line.foreignStore = foreignStore
1859 line.conditions = []
1860
1861 if (entry.type == 'lookup') {
1862 line.conditions.push({
1863 foreignProperty: foreignStore + '.' + entry.store.idProperty,
1864 localProperty: storeName + '.' + entry.localField
1865 })
1866 } else {
1867 Object.keys(entry.join).forEach(function (joinField) {
1868 line.conditions.push({
1869 foreignProperty: foreignStore + '.' + joinField,
1870 localProperty: storeName + '.' + entry.join[ joinField ]
1871 })
1872 })
1873 }
1874 nested.push(line)
1875 })
1876 }
1877
1878 rn.position = !!s.position
1879 if (s[ 'item-doc']) rn['item-doc'] = f.call(s, 'item-doc')
1880 if (s[ 'schema-doc']) rn['schema-doc'] = f.call(s, 'schema-doc')
1881
1882 rn.paramIds = s.paramIds
1883
1884 if (s.schema) {
1885 // Copy over the schema, taking out the docs parts
1886 rn.schema = _co(s.schema.structure)
1887 if (s['schema-extras-doc']) {
1888 for (var k in s['schema-extras-doc']) {
1889 rn.schema[ k ] = s['schema-extras-doc'][ k ]
1890 rn.schema[ k ].computed = true
1891 }
1892 }
1893 } else {
1894 rn.schema = {}
1895 }
1896
1897 if (s.getFullPublicURL) rn.fullPublicURL = s.getFullPublicURL()
1898
1899 // Go through each method, document each one based on the store itself
1900 rn.methods = {}
1901
1902 rn.HTTPresponses = {
1903 // OK : { status: 200, data: s['item-return-doc'], contentType: 'application/json', doc: `where item1, item2 are the full items` },
1904 NotImplementedError: { status: 501, data: s['error-format-doc'], contentType: 'application/json', doc: "The method requested isn't implemented" },
1905 UnauthorizedError: { status: 401, data: s['error-format-doc'], contentType: 'application/json', doc: 'Authentication is necessary before accessing this resource' },
1906 ForbiddenError: { status: 403, data: s['error-format-doc'], contentType: 'application/json', doc: 'Access to the resource was forbidden' },
1907 BadRequestError: { status: 400, data: s['error-format-doc'], contentType: 'application/json', doc: 'Some IDs in the URL are in the wrong format' },
1908 ServiceUnavailableError: { status: 503, data: s['error-format-doc'], contentType: 'application/json', doc: 'An unexpected error happened'},
1909 UnprocessableEntityError: { status: 422, data: s['error-format-doc'], contentType: 'application/json', doc: 'One of the parameters in the body has errors' }
1910 };
1911
1912 [ 'getQuery', 'get', 'put', 'post', 'delete'].forEach(function (method) {
1913 switch (method) {
1914
1915 case 'getQuery':
1916 if (!s.handleGetQuery) break
1917 var rnm = rn.methods[ method ] = {}
1918 if (s.publicURL) rnm.url = s.getFullPublicURL().replace(/\/:\w*$/, '')
1919 if (s.onlineSearchSchema) {
1920 rnm.onlineSearchSchema = s._co(s.onlineSearchSchema.structure)
1921 }
1922 if (s[ 'search-doc']) rnm['search-doc'] = f.call(s, 'search-doc')
1923 if (s[ 'sortableFields']) rnm.sortableFields = s[ 'sortableFields']
1924 if (s[ 'defaultSort']) rnm.defaultSort = s[ 'defaultSort']
1925 rnm.deleteFetchedRecords = !!s[ 'deleteAfterGetQuery' ]
1926 rnm.permissions = f.call(s, 'permissions-doc.getQuery')
1927
1928 rnm.HTTPresponses = {
1929 OK: { status: 200, data: '[ ...item... , ...item... ]', contentType: 'application/json', doc: 'The items as an array' },
1930 ServiceUnavailableError: rn.HTTPresponses.ServiceUnavailableError
1931 }
1932 if (!s[ 'disable-authresponses-doc']) {
1933 rnm.HTTPresponses.UnauthorizedError = rn.HTTPresponses.UnauthorizedError
1934 rnm.HTTPresponses.ForbiddenError = rn.HTTPresponses.ForbiddenError
1935 }
1936 if (s.paramIds.length != 1) {
1937 rnm.HTTPresponses.BadRequestError = rn.HTTPresponses.BadRequestError
1938 }
1939
1940 break
1941
1942 case 'get':
1943 if (!s.handleGet && (!s._singleFields || !Object.keys(s._singleFields).length)) break
1944 var rnm = rn.methods[ method ] = {}
1945 if (s.publicURL) rnm.url = s.getFullPublicURL()
1946 if (!s.handleGet) rnm.onlySingleFields = true
1947 rnm.permissions = f.call(s, 'permissions-doc.get')
1948
1949 rnm.HTTPresponses = {
1950 OK: { status: 200, data: '{ ...item... }', contentType: 'application/json', doc: 'The item as stored on the server' },
1951 ServiceUnavailableError: rn.HTTPresponses.ServiceUnavailableError
1952 }
1953 if (!s[ 'disable-authresponses-doc']) {
1954 rnm.HTTPresponses.UnauthorizedError = rn.HTTPresponses.UnauthorizedError
1955 rnm.HTTPresponses.ForbiddenError = rn.HTTPresponses.ForbiddenError
1956 }
1957 if (s.paramIds.length != 1) {
1958 rnm.HTTPresponses.BadRequestError = rn.HTTPresponses.BadRequestError
1959 }
1960
1961 break
1962
1963 case 'put':
1964 if (!s.handlePut && (!s._singleFields || !Object.keys(s._singleFields).length)) break
1965 var rnm = rn.methods[ method ] = {}
1966 if (!s.handlePut) rnm.onlySingleFields = true
1967 if (s.publicURL) rnm.url = s.getFullPublicURL()
1968 rnm.permissions = f.call(s, 'permissions-doc.put')
1969 rnm.echo = !!s.echoAfterPut
1970
1971 var i = s.echoAfterPut ? '{ ...item... }' : ''
1972 var d = s.echoAfterPut ? 'The item as stored on the server' : 'Nothing (echo is off)'
1973 rnm.HTTPresponses = {
1974 OK: { status: 200, data: i, contentType: 'application/json', doc: d },
1975 ServiceUnavailableError: rn.HTTPresponses.ServiceUnavailableError
1976 }
1977 if (!s[ 'disable-authresponses-doc']) {
1978 rnm.HTTPresponses.UnauthorizedError = rn.HTTPresponses.UnauthorizedError
1979 rnm.HTTPresponses.ForbiddenError = rn.HTTPresponses.ForbiddenError
1980 rnm.HTTPresponses.UnprocessableEntityError = rn.HTTPresponses.UnprocessableEntityError
1981 rnm.HTTPresponses.BadRequestError = rn.HTTPresponses.BadRequestError
1982 }
1983 break
1984
1985 case 'post':
1986 if (!s.handlePost) break
1987 var rnm = rn.methods[ method ] = {}
1988 if (s.publicURL) rnm.url = s.getFullPublicURL().replace(/\/:\w*$/, '')
1989
1990 rnm.permissions = f.call(s, 'permissions-doc.post')
1991 rnm.echo = !!s.echoAfterPost
1992
1993 var i = s.echoAfterPost ? '{ ...item... }' : ''
1994 var d = s.echoAfterPost ? 'The item as stored on the server' : 'Nothing (echo is off)'
1995 rnm.HTTPresponses = {
1996 OK: { status: 200, data: i, contentType: 'application/json', doc: d },
1997 ServiceUnavailableError: rn.HTTPresponses.ServiceUnavailableError
1998 }
1999 if (!s[ 'disable-authresponses-doc']) {
2000 rnm.HTTPresponses.UnauthorizedError = rn.HTTPresponses.UnauthorizedError
2001 rnm.HTTPresponses.ForbiddenError = rn.HTTPresponses.ForbiddenError
2002 rnm.HTTPresponses.UnprocessableEntityError = rn.HTTPresponses.UnprocessableEntityError
2003 }
2004 if (s.paramIds && s.paramIds.length != 1) {
2005 rnm.HTTPresponses.BadRequestError = rn.HTTPresponses.BadRequestError
2006 }
2007 break
2008
2009 case 'delete':
2010 if (!s.handleDelete) break
2011 var rnm = rn.methods[ method ] = {}
2012 if (s.publicURL) rnm.url = s.getFullPublicURL()
2013 rnm.permissions = f.call(s, 'permissions-doc.delete')
2014 rnm.echo = !!s.echoAfterDelete
2015
2016 var i = s.echoAfterDelete ? '{ ...item... }' : ''
2017 var d = s.echoAfterDelete ? 'The item as stored on the server before deletion' : 'Nothing (echo is off)'
2018 rnm.HTTPresponses = {
2019 OK: { status: 200, data: i, contentType: 'application/json', doc: d },
2020 ServiceUnavailableError: rn.HTTPresponses.ServiceUnavailableError
2021 }
2022 if (!s[ 'disable-authresponses-doc']) {
2023 rnm.HTTPresponses.UnauthorizedError = rn.HTTPresponses.UnauthorizedError
2024 rnm.HTTPresponses.ForbiddenError = rn.HTTPresponses.ForbiddenError
2025 }
2026 if (s.paramIds.length != 1) {
2027 rnm.HTTPresponses.BadRequestError = rn.HTTPresponses.BadRequestError
2028 }
2029 break
2030
2031 }
2032 })
2033
2034 callAll(s, 'changeDoc', rn)
2035
2036 rn.hasMethods = !!Object.keys(rn.methods).length
2037
2038 return rn
2039}
2040
2041/* Make full documentation data for the stores */
2042Store.makeDocsData = function () {
2043 var r = {}, rn, proto;
2044 [].slice.call(arguments).forEach(function (storesHash) {
2045 Object.keys(storesHash).forEach(function (storeName) {
2046 var s = storesHash[ storeName ]
2047 if (r[ storeName ]) throw new Error('makeDocsData: You cannot have two stores with the same name!')
2048 r[ storeName ] = Store.document(s)
2049 })
2050 })
2051 return r
2052}