UNPKG

39.3 kBJavaScriptView Raw
1/**
2 * @license
3 * MOST Web Framework 2.0 Codename Blueshift
4 * Copyright (c) 2017, THEMOST LP All rights reserved
5 *
6 * Use of this source code is governed by an BSD-3-Clause license that can be
7 * found in the LICENSE file at https://themost.io/license
8 */
9var sprintf = require('sprintf').sprintf;
10var HttpController = require('../mvc').HttpController;
11var _ = require('lodash');
12var pluralize = require('pluralize');
13var TraceUtils = require('@themost/common/utils').TraceUtils;
14var LangUtils = require('@themost/common/utils').LangUtils;
15var HttpError = require('@themost/common/errors').HttpError;
16var HttpServerError = require('@themost/common/errors').HttpServerError;
17var HttpMethodNotAllowedError = require('@themost/common/errors').HttpMethodNotAllowedError;
18var HttpBadRequestError = require('@themost/common/errors').HttpBadRequestError;
19var HttpNotFoundError = require('@themost/common/errors').HttpNotFoundError;
20
21/**
22 * @classdesc HttpDataController class describes a common MOST Web Framework data controller.
23 * This controller is inherited by default from all data models. It offers a set of basic actions for CRUD operations against data objects
24 * and allows filtering, paging, sorting and grouping data objects with options similar to [OData]{@link http://www.odata.org/}.
25 <h2>Basic Features</h2>
26 <h3>Data Filtering ($filter query option)</h3>
27 <p>Logical Operators</p>
28 <p>The following table contains the logical operators supported in the query language:</p>
29 <table class="table-flat">
30 <thead><tr><th>Operator</th><th>Description</th><th>Example</th></tr></thead>
31 <tbody>
32 <tr><td>eq</td><td>Equal</td><td>/Order/index.json?$filter=customer eq 353</td></tr>
33 <tr><td>ne</td><td>Not Equal</td><td>/Order/index.json?$filter=orderStatus/alternateName ne 'OrderDelivered'</td></tr>
34 <tr><td>gt</td><td>Greater than</td><td>/Order/index.json?$filter=orderedItem/price gt 1000</td></tr>
35 <tr><td>ge</td><td>Greater than or equal</td><td>/Order/index.json?$filter=orderedItem/price ge 500</td></tr>
36 <tr><td>lt</td><td>Lower than</td><td>/Order/index.json?$filter=orderedItem/price lt 500</td></tr>
37 <tr><td>le</td><td>Lower than or equal</td><td>/Order/index.json?$filter=orderedItem/price le 1000</td></tr>
38 <tr><td>and</td><td>Logical and</td><td>/Order/index.json?$filter=orderedItem/price gt 1000 and orderStatus/alternateName eq 'OrderPickup'</td></tr>
39 <tr><td>or</td><td>Logical or</td><td>/Order/index.json?$filter=orderStatus/alternateName eq 'OrderPickup' or orderStatus/alternateName eq 'OrderProcessing'</td></tr>
40 </tbody>
41 </table>
42 <p>Arithmetic Operators</p>
43 <p>The following table contains the arithmetic operators supported in the query language:</p>
44 <table class="table-flat">
45 <thead><tr><th>Operator</th><th>Description</th><th>Example</th></tr></thead>
46 <tbody>
47 <tr><td>add</td><td>Addition</td><td>/Order/index.json?$filter=(orderedItem/price add 10) gt 1560</td></tr>
48 <tr><td>sub</td><td>Subtraction</td><td>/Order/index.json?$filter=(orderedItem/price sub 10) gt 1540</td></tr>
49 <tr><td>mul</td><td>Multiplication</td><td>/Order/index.json?$filter=(orderedItem/price mul 1.20) gt 1000</td></tr>
50 <tr><td>div</td><td>Division</td><td>/Order/index.json?$filter=(orderedItem/price div 2) le 500</td></tr>
51 <tr><td>mod</td><td>Modulo</td><td>/Order/index.json?$filter=(orderedItem/price mod 2) eq 0</td></tr>
52 </tbody>
53 </table>
54 <p>Functions</p>
55 <p>A set of functions are also defined for use in $filter query option:</p>
56 <table class="table-flat">
57 <thead><tr><th>Function</th><th>Example</th></tr></thead>
58 <tbody>
59 <tr><td colspan="2"><b>String Functions</b></td></tr>
60 <tr><td>startswith(field,string)</td><td>/Product/index.json?$filter=startswith(name,'Apple') eq true</td></tr>
61 <tr><td>endswith(field,string)</td><td>/Product/index.json?$filter=endswith(name,'Workstation') eq true</td></tr>
62 <tr><td>contains(field,string)</td><td>/Product/index.json?$filter=contains(name,'MacBook') eq true</td></tr>
63 <tr><td>length(field)</td><td>/Product/index.json?$filter=length(name) gt 40</td></tr>
64 <tr><td>indexof(field,string)</td><td>/Product/index.json?$filter=indexof(name,'Air') gt 1</td></tr>
65 <tr><td>substring(field,number)</td><td>/Product/index.json?$filter=substring(category,1) eq 'aptops'</td></tr>
66 <tr><td>substring(field,number,number)</td><td>/Product/index.json?$filter=substring(category,1,2) eq 'ap'</td></tr>
67 <tr><td>tolower(field)</td><td>/Product/index.json?$filter=tolower(category) eq 'laptops'</td></tr>
68 <tr><td>toupper(field)</td><td>/Product/index.json?$filter=toupper(category) eq 'LAPTOPS'</td></tr>
69 <tr><td>trim(field)</td><td>/Product/index.json?$filter=trim(category) eq 'Laptops'</td></tr>
70 <tr><td colspan="2"><b>Date Functions</b></td></tr>
71 <tr><td>day(field)</td><td>/Order/index.json?$filter=day(orderDate) eq 4</td></tr>
72 <tr><td>month(field)</td><td>/Order/index.json?$filter=month(orderDate) eq 6</td></tr>
73 <tr><td>year(field)</td><td>/Order/index.json?$filter=year(orderDate) ge 2014</td></tr>
74 <tr><td>hour(field)</td><td>/Order/index.json?$filter=hour(orderDate) ge 12 and hour(orderDate) lt 14</td></tr>
75 <tr><td>minute(field)</td><td>/Order/index.json?$filter=minute(orderDate) gt 15 and minute(orderDate) le 30</td></tr>
76 <tr><td>second(field)</td><td>/Order/index.json?$filter=second(orderDate) ge 0 and second(orderDate) le 45</td></tr>
77 <tr><td>date(field)</td><td>/Order/index.json?$filter=date(orderDate) eq '2015-03-20'</td></tr>
78 <tr><td colspan="2"><b>Math Functions</b></td></tr>
79 <tr><td>round(field)</td><td>/Product/index.json?$filter=round(price) le 389</td></tr>
80 <tr><td>floor(field)</td><td>/Product/index.json?$filter=floor(price) eq 389</td></tr>
81 <tr><td>ceiling(field)</td><td>/Product/index.json?$filter=ceiling(price) eq 390</td></tr>
82 </tbody>
83 </table>
84 <h3>Attribute Selection ($select query option)</h3>
85 <p>The following table contains attribute selection expressions supported in the query language:</p>
86 <table class="table-flat">
87 <thead><tr><th>Description</th><th>Example</th></tr></thead>
88 <tbody>
89 <tr><td>Select attribute</td><td>/Order/index.json?$select=id,customer,orderStatus</td></tr>
90 <tr><td>Select attribute with alias</td><td>/Order/index.json?$select=id,customer/description as customerName,orderStatus/name as orderStatusName</td></tr>
91 <tr><td>Select attribute with aggregation</td><td>/Order/index.json?$select=count(id) as totalCount&$filter=orderStatus/alternateName eq 'OrderProcessing'</td></tr>
92 <tr><td>&nbsp;</td><td>/Product/index.json?$select=max(price) as maxPrice&$filter=category eq 'Laptops'</td></tr>
93 <tr><td>&nbsp;</td><td>/Product/index.json?$select=min(price) as minPrice&$filter=category eq 'Laptops'</td></tr>
94 </tbody>
95 </table>
96 <h3>Data Sorting ($orderby or $order query options)</h3>
97 <table class="table-flat">
98 <thead><tr><th>Description</th><th>Example</th></tr></thead>
99 <tbody>
100 <tr><td>Ascending order</td><td>/Product/index.json?$orderby=name</td></tr>
101 <tr><td>Descending order</td><td>/Product/index.json?$orderby=category desc,name desc</td></tr>
102 </tbody>
103 </table>
104 <h3>Data Paging ($top, $skip and $inlinecount query options)</h3>
105 <p>The $top query option allows developers to apply paging in the result-set by giving the max number of records for each page. The default value is 25.
106 The $skip query option provides a way to skip a number of records. The default value is 0.
107 The $inlinecount query option includes in the result-set the total number of records of the query expression provided:
108 <pre class="prettyprint"><code>
109 {
110 "total": 94,
111 "value": [ ... ]
112 }
113 </code></pre>
114 <p>The default value is false.</p>
115 </p>
116 <table class="table-flat">
117 <thead><tr><th>Description</th><th>Example</th></tr></thead>
118 <tbody>
119 <tr><td>Limit records</td><td>/Product/index.json?$top=5</td></tr>
120 <tr><td>Skip records</td><td>/Product/index.json?$top=5&$skip=5</td></tr>
121 <tr><td>Paged records</td><td>/Product/index.json?$top=5&$skip=5&$inlinecount=true</td></tr>
122 </tbody>
123 </table>
124 <h3>Data Grouping ($groupby or $group query options)</h3>
125 <p>The $groupby query option allows developers to group the result-set by one or more attributes</p>
126 <table class="table-flat">
127 <thead><tr><th>Description</th><th>Example</th></tr></thead>
128 <tbody>
129 <tr><td>group</td><td>/Product/index.json?$select=count(id) as totalCount,category&$groupby=category</td></tr>
130 <tr><td>group and sort</td><td>/Product/index.json?$select=count(id) as totalCount,category&$groupby=category&$orderby=count(id) desc</td></tr>
131 </tbody>
132 </table>
133 <h3>Data Expanding ($expand)</h3>
134 <p>The $expand query option forces response to include associated objects which are not marked as expandable by default.</p>
135 <table class="table-flat">
136 <thead><tr><th>Description</th><th>Example</th></tr></thead>
137 <tbody>
138 <tr><td>expand</td><td>/Order/index.json?$filter=orderStatus/alternateName eq 'OrderProcessing'&$expand=customer</td></tr>
139 </tbody>
140 </table>
141 <p>The $expand option is optional for a <a href="https://docs.themost.io/most-data/DataField.html">DataField</a> marked as expandable.</p>
142 * @class
143 * @constructor
144 * @augments HttpController
145 * @property {DataModel} model - Gets or sets the current data model.
146 */
147function HttpDataController()
148{
149 var model_;
150 var self = this;
151 Object.defineProperty(this, 'model', {
152 get: function() {
153 if (model_)
154 return model_;
155 model_ = self.context.model(self.name);
156 return model_;
157 },
158 set: function(value) {
159 model_ = value;
160 },
161 configurable:true,
162 enumerable:false
163 });
164}
165LangUtils.inherits(HttpDataController, HttpController);
166
167/**
168 * Handles data object creation (e.g. /user/1/new.html, /user/1/new.json etc)
169 * @param {Function} callback
170 */
171HttpDataController.prototype.new = function (callback) {
172 try {
173 var self = this,
174 context = self.context;
175 context.handle(['GET'],function() {
176 callback(null, self.result());
177 }).handle(['POST', 'PUT'],function() {
178 var target = self.model.convert(context.params[self.model.name] || context.params.data, true);
179 self.model.save(target, function(err)
180 {
181 if (err) {
182 callback(HttpError.create(err));
183 }
184 else {
185 if (context.params.attr('returnUrl'))
186 callback(null, context.params.attr('returnUrl'));
187 callback(null, self.result(target));
188 }
189 });
190 }).unhandle(function() {
191 callback(new HttpMethodNotAllowedError());
192 });
193 }
194 catch (e) {
195 callback(HttpError.create(e));
196 }
197};
198/**
199 * Handles data object edit (e.g. /user/1/edit.html, /user/1/edit.json etc)
200 * @param {Function} callback
201 */
202HttpDataController.prototype.edit = function (callback) {
203 try {
204 var self = this,
205 context = self.context;
206 context.handle(['POST', 'PUT'], function() {
207 //get context param
208 var target = self.model.convert(context.params[self.model.name] || context.params.data, true);
209 if (target) {
210 self.model.save(target, function(err)
211 {
212 if (err) {
213 TraceUtils.log(err);
214 TraceUtils.log(err.stack);
215 callback(HttpError.create(err));
216 }
217 else {
218 if (context.params.attr('returnUrl'))
219 callback(null, context.params.attr('returnUrl'));
220 callback(null, self.result(target));
221 }
222 });
223 }
224 else {
225 callback(new HttpBadRequestError());
226 }
227 }).handle('DELETE', function() {
228 //get context param
229 var target = context.params[self.model.name] || context.params.data;
230 if (target) {
231 self.model.remove(target, function(err)
232 {
233 if (err) {
234 callback(HttpError.create(err));
235 }
236 else {
237 if (context.params.attr('returnUrl'))
238 callback(null, context.params.attr('returnUrl'));
239 callback(null, self.result(null));
240 }
241 });
242 }
243 else {
244 callback(new HttpBadRequestError());
245 }
246 }).handle('GET', function() {
247 if (context.request.route) {
248 if (context.request.route.static) {
249 callback(null, self.result());
250 return;
251 }
252 }
253 //get context param (id)
254 var filter = null, id = context.params.attr('id');
255 if (id) {
256 //create the equivalent open data filter
257 return self.model.where(self.model.primaryKey).equal(id).getItem().then(function(result) {
258 if (_.isNil(result)) {
259 return callback(null, self.result());
260 }
261 return callback(null, self.result(result));
262 }).catch(function(err) {
263 return callback(err);
264 });
265 }
266 else {
267 //get the requested open data filter
268 filter = context.params.attr('$filter');
269 }
270 if (filter) {
271 self.model.filter(filter, function(err, q) {
272 if (err) {
273 callback(HttpError.create(err));
274 return;
275 }
276 q.take(1, function (err, result) {
277 try {
278 if (err) {
279 callback(err);
280 }
281 else {
282 if (result.length>0)
283 callback(null, self.result(result));
284 else
285 callback(null, self.result(null));
286 }
287 }
288 catch (e) {
289 callback(HttpError.create(e));
290 }
291 });
292 });
293 }
294 else {
295 callback(new HttpBadRequestError());
296 }
297
298 }).unhandle(function() {
299 callback(new HttpMethodNotAllowedError());
300 });
301
302 }
303 catch (e) {
304 callback(HttpError.create(e));
305 }
306
307};
308
309HttpDataController.prototype.schema = function (callback) {
310 var self = this, context = self.context;
311 context.handle('GET', function() {
312 if (self.model) {
313 //prepare client model
314 var clone = JSON.parse(JSON.stringify(self.model));
315 var m = _.assign({}, clone);
316 //delete private properties
317 var keys = Object.keys(m);
318 for (var i = 0; i < keys.length; i++) {
319 var key = keys[i];
320 if (key.indexOf("_") === 0)
321 delete m[key];
322 }
323 //delete other server properties
324 delete m.view;
325 delete m.source;
326 delete m.fields;
327 delete m.privileges;
328 delete m.constraints;
329 delete m.eventListeners;
330 //set fields equal attributes
331 m.attributes = JSON.parse(JSON.stringify(self.model.attributes));
332 m.attributes.forEach(function(x) {
333 var mapping = self.model.inferMapping(x.name);
334 if (mapping)
335 x.mapping = JSON.parse(JSON.stringify(mapping));
336 //delete private properties
337 delete x.value;
338 delete x.calculation;
339 });
340 //prepare views and view fields
341 if (m.views) {
342 m.views.forEach(function(view) {
343 if (view.fields) {
344 view.fields.forEach(function(field) {
345 if (/\./.test(field.name)===false) {
346 //extend view field
347 var name = field.name;
348 var mField = m.attributes.filter(function(y) {
349 return (y.name===name);
350 })[0];
351 if (mField) {
352 for (var key in mField) {
353 if (mField.hasOwnProperty(key) && !field.hasOwnProperty(key)) {
354 field[key] = mField[key];
355 }
356 }
357 }
358 }
359 });
360 }
361 });
362 }
363 callback(null, self.result(m));
364 }
365 else {
366 callback(new HttpNotFoundError());
367 }
368
369 }).unhandle(function() {
370 callback(new HttpMethodNotAllowedError());
371 });
372};
373
374/**
375 * Handles data object display (e.g. /user/1/show.html, /user/1/show.json etc)
376 * @param {Function} callback
377 */
378HttpDataController.prototype.show = function (callback) {
379 try {
380 var self = this, context = self.context;
381 context.handle('GET', function() {
382 if (context.request.route) {
383 if (context.request.route.static) {
384 callback(null, self.result());
385 return;
386 }
387 }
388 var filter = null, id = context.params.attr('id');
389 if (id) {
390 //create the equivalent open data filter
391 filter = sprintf('%s eq %s',self.model.primaryKey,id);
392 }
393 else {
394 //get the requested open data filter
395 filter = context.params.attr('$filter');
396 }
397 self.model.filter(filter, function(err, q) {
398 if (err) {
399 callback(HttpError.create(err));
400 return;
401 }
402 q.take(1, function (err, result) {
403 try {
404 if (err) {
405 callback(HttpError.create(err));
406 }
407 else {
408 if (result.length>0)
409 callback(null, self.result(result[0]));
410 else
411 callback(new HttpNotFoundError('Item Not Found'));
412 }
413 }
414 catch (e) {
415 callback(HttpError.create(e));
416 }
417 });
418 });
419 }).unhandle(function() {
420 callback(new HttpMethodNotAllowedError());
421 });
422 }
423 catch (e) {
424 callback(e);
425 }
426};
427/**
428 * Handles data object deletion (e.g. /user/1/remove.html, /user/1/remove.json etc)
429 * @param {Function} callback
430 */
431HttpDataController.prototype.remove = function (callback) {
432 try {
433 var self = this, context = self.context;
434 context.handle(['POST','DELETE'], function() {
435 var target = context.params[self.model.name] || context.params.data;
436 if (target) {
437 self.model.remove(target, function(err)
438 {
439 if (err) {
440 callback(HttpError.create(err));
441 }
442 else {
443 if (context.params.attr('returnUrl'))
444 callback(null, context.params.attr('returnUrl'));
445 callback(null, self.result(target));
446 }
447 });
448 }
449 else {
450 callback(new HttpBadRequestError());
451 }
452 }).unhandle(function() {
453 callback(new HttpMethodNotAllowedError());
454 });
455 }
456 catch (e) {
457 callback(HttpError.create(e))
458 }
459};
460
461/**
462 * @param {Function} callback
463 * @private
464 */
465HttpDataController.prototype.filter = function (callback) {
466
467 var self = this, params = self.context.params;
468 if (typeof self.model !== 'object' || self.model === null) {
469 callback(new Error('Model is of the wrong type or undefined.'));
470 return;
471 }
472
473 var filter = params.$filter,
474 select = params.$select,
475 search = params.$search,
476 skip = params.$skip || 0,
477 levels = parseInt(params.$levels),
478 orderBy = params.$order || params.$orderby,
479 groupBy = params.$group || params.$groupby,
480 expand = params.$expand;
481
482 self.model.filter(filter,
483 /**
484 * @param {Error} err
485 * @param {DataQueryable} q
486 */
487 function (err, q) {
488 try {
489 if (err) {
490 return callback(err);
491 }
492 else {
493 if ((typeof search === 'string') && (search.length>0)) {
494 q.search(search);
495 }
496 //set $groupby
497 if (groupBy) {
498 q.groupBy.apply(q, groupBy.split(',').map(function(x) {
499 return x.replace(/^\s+|\s+$/g, '');
500 }));
501 }
502 //set $select
503 if (select) {
504 q.select.apply(q, select.split(',').map(function(x) {
505 return x.replace(/^\s+|\s+$/g, '');
506 }));
507 }
508 //set $skip
509 if (!/^\d+$/.test(skip)) {
510 return callback(new HttpBadRequestError("Skip may be a non-negative integer."))
511 }
512 //set expandable levels
513 if (!isNaN(levels)) {
514 q.levels(levels);
515 }
516 q.skip(skip);
517 //set $orderby
518 if (orderBy) {
519 orderBy.split(',').map(function(x) {
520 return x.replace(/^\s+|\s+$/g, '');
521 }).forEach(function(x) {
522 if (/\s+desc$/i.test(x)) {
523 q.orderByDescending(x.replace(/\s+desc$/i, ''));
524 }
525 else if (/\s+asc/i.test(x)) {
526 q.orderBy(x.replace(/\s+asc/i, ''));
527 }
528 else {
529 q.orderBy(x);
530 }
531 });
532 }
533 if (expand) {
534 var resolver = require("@themost/data/data-expand-resolver");
535 var matches = resolver.testExpandExpression(expand);
536 if (matches && matches.length>0) {
537 q.expand.apply(q, matches);
538 }
539 }
540 //return
541 callback(null, q);
542 }
543 }
544 catch (e) {
545 callback(e);
546 }
547 });
548};
549/**
550 *
551 * @param {Function} callback
552 */
553HttpDataController.prototype.index = function(callback)
554{
555
556 try {
557 var self = this, context = self.context,
558 top = parseInt(context.params.attr('$top')),
559 take = top > 0 ? top : (top === -1 ? top : 25);
560 var count = /^true$/ig.test(context.params.attr('$inlinecount')) || /^true$/ig.test(context.params.attr('$count')) || false;
561 var first = /^true$/ig.test(context.params.attr('$first')) || false;
562 var asArray = /^true$/ig.test(context.params.attr('$array')) || false;
563 TraceUtils.debug(context.request.url);
564 context.handle('GET', function() {
565 if (context.request.route) {
566 if (context.request.route.static) {
567 callback(null, self.result([]));
568 return;
569 }
570 }
571 self.filter(
572 /**
573 * @param {Error} err
574 * @param {DataQueryable=} q
575 */
576 function(err, q) {
577 try {
578 if (err) {
579 return callback(HttpError.create(err));
580 }
581 //apply as array parameter
582 q.asArray(asArray);
583 if (first) {
584 return q.first().then(function(result) {
585 return callback(null, self.result(result));
586 }).catch(function(err) {
587 return callback(HttpError.create(err));
588 });
589 }
590
591 if (take<0) {
592 return q.all().then(function(result) {
593 if (count) {
594 return callback(null, self.result({
595 value:result,
596 total:result.length
597 }));
598 }
599 else {
600 return callback(null, self.result(result));
601 }
602 }).catch(function(err) {
603 return callback(HttpError.create(err));
604 });
605 }
606 else {
607 if (count) {
608 return q.take(take).list().then(function(result) {
609 return callback(null, self.result(result));
610 }).catch(function(err) {
611 return callback(HttpError.create(err));
612 });
613 }
614 else {
615 return q.take(take).getItems().then(function(result) {
616 return callback(null, self.result(result));
617 }).catch(function(err) {
618 return callback(HttpError.create(err));
619 });
620 }
621 }
622 }
623 catch (e) {
624 return callback(e);
625 }
626 });
627 }).handle(['POST', 'PUT'], function() {
628 var target;
629 try {
630 target = self.model.convert(context.params[self.model.name] || context.params.data, true);
631 }
632 catch(err) {
633 TraceUtils.log(err);
634 var er = new HttpError(422, "An error occured while converting data objects.", err.message);
635 er.code = 'EDATA';
636 return callback(er);
637 }
638 if (target) {
639 self.model.save(target, function(err)
640 {
641 if (err) {
642 TraceUtils.log(err);
643 callback(HttpError.create(err));
644 }
645 else {
646 callback(null, self.result(target));
647 }
648 });
649 }
650 else {
651 return callback(new HttpBadRequestError());
652 }
653 }).handle('DELETE', function() {
654 //get data
655 var target;
656 try {
657 target = self.model.convert(context.params[self.model.name] || context.params.data, true);
658 }
659 catch(err) {
660 TraceUtils.log(err);
661 var er = new HttpError(422, "An error occurred while converting data objects.", err.message);
662 er.code = 'EDATA';
663 return callback(er);
664 }
665 if (target) {
666 self.model.remove(target, function(err)
667 {
668 if (err) {
669 callback(HttpError.create(err));
670 }
671 else {
672 callback(null, self.result(target));
673 }
674 });
675 }
676 else {
677 return callback(new HttpBadRequestError());
678 }
679 }).unhandle(function() {
680 return callback(new HttpMethodNotAllowedError());
681 });
682 }
683 catch (e) {
684 callback(HttpError.create(e));
685 }
686};
687/**
688 * Returns an instance of HttpResult class which contains a collection of items based on the specified association.
689 * This association should be a one-to-many association or many-many association.
690 * A routing for this action may be:
691 <pre class="prettyprint"><code>
692 { "url":"/:controller/:parent/:model/index.json", "mime":"application/json", "action":"association" }
693 </code></pre>
694 <p>
695 or
696 </p>
697 <pre class="prettyprint"><code>
698 { "url":"/:controller/:parent/:model/index.html", "mime":"text/html", "action":"association" }
699 </code></pre>
700 <pre class="prettyprint"><code>
701 //get orders in JSON format
702 /GET /Party/353/Order/index.json
703 </code></pre>
704 <p>
705 This action supports common query options like $filter, $order, $top, $skip etc.
706 The result will be a result-set with associated items:
707 </p>
708 <pre class="prettyprint"><code>
709 //JSON Results:
710 {
711 "total": 8,
712 "skip": 0,
713 "value": [
714 {
715 "id": 37,
716 "customer": 353,
717 "orderDate": "2015-05-05 01:19:34.000+03:00",
718 "orderedItem": {
719 "id": 407,
720 "additionalType": "Product",
721 "category": "PC Components",
722 "price": 1625.49,
723 "model": "HR5845",
724 "releaseDate": "2015-09-20 03:35:33.000+03:00",
725 "name": "Nvidia GeForce GTX 650 Ti Boost",
726 "dateCreated": "2015-11-23 14:53:04.884+02:00",
727 "dateModified": "2015-11-23 14:53:04.887+02:00"
728 },
729 "orderNumber": "OFV804",
730 "orderStatus": {
731 "id": 1,
732 "name": "Delivered",
733 "alternateName": "OrderDelivered",
734 "description": "Representing the successful delivery of an order."
735 },
736 "paymentDue": "2015-05-25 01:19:34.000+03:00",
737 "paymentMethod": {
738 "id": 6,
739 "name": "Direct Debit",
740 "alternateName": "DirectDebit",
741 "description": "Payment by direct debit"
742 },
743 "additionalType": "Order",
744 "dateCreated": "2015-11-23 21:00:18.264+02:00",
745 "dateModified": "2015-11-23 21:00:18.266+02:00"
746 }
747 ...]
748 ...
749}
750</code></pre>
751 * @param {Function} callback - A callback function where the first argument will contain the Error object if an error occured, or null otherwise.
752 */
753HttpDataController.prototype.association = function(callback) {
754 try {
755 var self = this,
756 parent = self.context.params.parent,
757 model = self.context.params.model;
758 if (_.isNil(parent) || _.isNil(model)) {
759 return callback(new HttpBadRequestError());
760 }
761 return self.model.where(self.model.primaryKey).equal(parent).select(self.model.primaryKey).getTypedItem()
762 .then(function(obj) {
763 if (_.isNil(obj)) {
764 return callback(new HttpNotFoundError());
765 }
766 //get primary key
767 var key = obj[self.model.primaryKey];
768 //get mapping
769 var mapping = self.model.inferMapping(model);
770 //get count parameter
771 var count = LangUtils.parseBoolean(self.context.params.$inlinecount);
772 if (_.isNil(mapping)) {
773 //try to find associated model
774 //get singular model name
775 var otherModelName = pluralize.singular(model);
776 //search for model with this name
777 var otherModel = self.context.model(otherModelName);
778 if (otherModel) {
779 var otherFields = _.filter(otherModel.attributes, function(x) {
780 return x.type === self.model.name;
781 });
782 if (otherFields.length>1) {
783 return callback(new HttpMethodNotAllowedError("Multiple associations found"));
784 }
785 else if (otherFields.length === 1) {
786 var otherField = otherFields[0];
787 mapping = otherModel.inferMapping(otherField.name);
788 if (mapping && mapping.associationType === 'junction') {
789 var attr;
790 //search model for attribute that has an association of type junction with child model
791 if (mapping.parentModel === otherModel.name) {
792 attr = _.find(otherModel.attributes, function(x) {
793 return x.name === otherField.name;
794 });
795 }
796 else {
797 attr = _.find(self.model.attributes, function(x) {
798 return x.type === otherModel.name;
799 });
800 }
801 if (_.isNil(attr)) {
802 return callback(new HttpNotFoundError("Association not found"));
803 }
804 if (attr) {
805 model = attr.name;
806 mapping = self.model.inferMapping(attr.name);
807 }
808 }
809 }
810 }
811 if (_.isNil(mapping)) {
812 return callback(new HttpNotFoundError("Association not found"));
813 }
814 }
815 if (mapping.associationType === 'junction') {
816 /**
817 * @type {DataQueryable}
818 */
819 var junction = obj.property(model);
820 return junction.model.filter(self.context.params, function (err, q) {
821 if (err) {
822 callback(err);
823 }
824 else {
825 //merge properties
826 if (q.query.$select) {
827 junction.query.$select = q.query.$select;
828 }
829 if (q.$expand) {
830 junction.$expand = q.$expand;
831 }
832 if (q.query.$group) {
833 junction.query.$group = q.query.$group;
834 }
835 if (q.query.$order) {
836 junction.query.$order = q.query.$order;
837 }
838 if (q.query.$prepared) {
839 junction.query.$where = q.query.$prepared;
840 }
841 if (q.query.$skip) {
842 junction.query.$skip = q.query.$skip;
843 }
844 if (q.query.$take) {
845 junction.query.$take = q.query.$take;
846 }
847 if (count) {
848 junction.list(function (err, result) {
849 if (err) {
850 return callback(err);
851 }
852 return callback(null, self.result(result));
853 });
854 }
855 else {
856 junction.getItems().then(function (result) {
857 return callback(null, self.result(result));
858 }).catch(function (err) {
859 return callback(err);
860 });
861 }
862
863 }
864 });
865 }
866 else if (mapping.parentModel === self.model.name && mapping.associationType === 'association') {
867 //get associated model
868 var associatedModel = self.context.model(mapping.childModel);
869 if (_.isNil(associatedModel)) {
870 return callback(new HttpNotFoundError("Associated model not found"));
871 }
872 return associatedModel.filter(self.context.params,
873 /**
874 * @param {Error} err
875 * @param {DataQueryable} q
876 * @returns {*}
877 */
878 function (err, q) {
879 if (err) {
880 return callback(err);
881 }
882 if (count) {
883 q.where(mapping.childField).equal(key).list(function (err, result) {
884 if (err) {
885 return callback(err);
886 }
887 return callback(null, self.result(result));
888 });
889 }
890 else {
891 q.where(mapping.childField).equal(key).getItems().then(function (result) {
892 return callback(null, self.result(result));
893 }).catch(function (err) {
894 return callback(err);
895 });
896 }
897 });
898 }
899 else {
900 return callback(new HttpNotFoundError());
901 }
902
903 }).catch(function (err) {
904 return callback(err);
905 });
906 }
907 catch(err) {
908 TraceUtils.log(err);
909 callback(err, new HttpServerError());
910 }
911};
912
913if (typeof module !== 'undefined') {
914 module.exports = HttpDataController;
915}
\No newline at end of file