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 | */
|
9 | var sprintf = require('sprintf').sprintf;
|
10 | var HttpController = require('../mvc').HttpController;
|
11 | var _ = require('lodash');
|
12 | var pluralize = require('pluralize');
|
13 | var TraceUtils = require('@themost/common/utils').TraceUtils;
|
14 | var LangUtils = require('@themost/common/utils').LangUtils;
|
15 | var HttpError = require('@themost/common/errors').HttpError;
|
16 | var HttpServerError = require('@themost/common/errors').HttpServerError;
|
17 | var HttpMethodNotAllowedError = require('@themost/common/errors').HttpMethodNotAllowedError;
|
18 | var HttpBadRequestError = require('@themost/common/errors').HttpBadRequestError;
|
19 | var 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> </td><td>/Product/index.json?$select=max(price) as maxPrice&$filter=category eq 'Laptops'</td></tr>
|
93 | <tr><td> </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 | */
|
147 | function 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 | }
|
165 | LangUtils.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 | */
|
171 | HttpDataController.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 | */
|
202 | HttpDataController.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 |
|
309 | HttpDataController.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 | */
|
378 | HttpDataController.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 | */
|
431 | HttpDataController.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 | */
|
465 | HttpDataController.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 | */
|
553 | HttpDataController.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 | */
|
753 | HttpDataController.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 |
|
913 | if (typeof module !== 'undefined') {
|
914 | module.exports = HttpDataController;
|
915 | } |
\ | No newline at end of file |