forms-angular 0.12.0-beta.2 → 0.12.0-beta.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,13 @@
1
- 'use strict';
1
+ "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FormsAngular = void 0;
3
4
  // This part of forms-angular borrows _very_ heavily from https://github.com/Alexandre-Strzelewicz/angular-bridge
4
5
  // (now https://github.com/Unitech/angular-bridge
5
- var _ = require('lodash');
6
- var util = require('util');
7
- var extend = require('node.extend');
8
- var async = require('async');
9
- var debug = false;
6
+ const _ = require('lodash');
7
+ const util = require('util');
8
+ const extend = require('node.extend');
9
+ const async = require('async');
10
+ let debug = false;
10
11
  function logTheAPICalls(req, res, next) {
11
12
  void (res);
12
13
  console.log('API : ' + req.method + ' ' + req.url + ' [ ' + JSON.stringify(req.body) + ' ]');
@@ -14,8 +15,8 @@ function logTheAPICalls(req, res, next) {
14
15
  }
15
16
  function processArgs(options, array) {
16
17
  if (options.authentication) {
17
- var authArray = _.isArray(options.authentication) ? options.authentication : [options.authentication];
18
- for (var i = authArray.length - 1; i >= 0; i--) {
18
+ let authArray = _.isArray(options.authentication) ? options.authentication : [options.authentication];
19
+ for (let i = authArray.length - 1; i >= 0; i--) {
19
20
  array.splice(1, 0, authArray[i]);
20
21
  }
21
22
  }
@@ -25,1026 +26,1310 @@ function processArgs(options, array) {
25
26
  array[0] = options.urlPrefix + array[0];
26
27
  return array;
27
28
  }
28
- var DataForm = function (mongoose, app, options) {
29
- this.app = app;
30
- app.locals.formsAngular = app.locals.formsAngular || [];
31
- app.locals.formsAngular.push(this);
32
- this.mongoose = mongoose;
33
- mongoose.set('debug', debug);
34
- mongoose.Promise = global.Promise;
35
- this.options = _.extend({
36
- urlPrefix: '/api/'
37
- }, options || {});
38
- this.resources = [];
39
- this.searchFunc = async.forEach;
40
- this.registerRoutes();
41
- this.app.get.apply(this.app, processArgs(this.options, ['search', this.searchAll()]));
42
- for (var pluginName in this.options.plugins) {
43
- var pluginObj = this.options.plugins[pluginName];
44
- this[pluginName] = pluginObj.plugin(this, processArgs, pluginObj.options);
29
+ class FormsAngular {
30
+ constructor(mongoose, app, options) {
31
+ this.mongoose = mongoose;
32
+ this.app = app;
33
+ app.locals.formsAngular = app.locals.formsAngular || [];
34
+ app.locals.formsAngular.push(this);
35
+ mongoose.set('debug', debug);
36
+ mongoose.Promise = global.Promise;
37
+ this.options = _.extend({
38
+ urlPrefix: '/api/'
39
+ }, options || {});
40
+ this.resources = [];
41
+ this.searchFunc = async.forEach;
42
+ const search = 'search/', schema = 'schema/', report = 'report/', resourceName = ':resourceName', id = '/:id', formName = '/:formName';
43
+ this.app.get.apply(this.app, processArgs(this.options, ['models', this.models()]));
44
+ this.app.get.apply(this.app, processArgs(this.options, [search + resourceName, this.search()]));
45
+ this.app.get.apply(this.app, processArgs(this.options, [schema + resourceName, this.schema()]));
46
+ this.app.get.apply(this.app, processArgs(this.options, [schema + resourceName + formName, this.schema()]));
47
+ this.app.get.apply(this.app, processArgs(this.options, [report + resourceName, this.report()]));
48
+ this.app.get.apply(this.app, processArgs(this.options, [report + resourceName + '/:reportName', this.report()]));
49
+ this.app.get.apply(this.app, processArgs(this.options, [resourceName, this.collectionGet()]));
50
+ this.app.post.apply(this.app, processArgs(this.options, [resourceName, this.collectionPost()]));
51
+ // return the List attributes for all records - used by record-handler's setUpLookupOptions() method, for cases
52
+ // where there's a lookup that doesn't use the fngajax option
53
+ this.app.get.apply(this.app, processArgs(this.options, [resourceName + '/listAll', this.entityListAll()]));
54
+ // return the List attributes for a record - used by fng-ui-select
55
+ this.app.get.apply(this.app, processArgs(this.options, [resourceName + id + '/list', this.entityList()]));
56
+ this.app.get.apply(this.app, processArgs(this.options, [resourceName + id, this.entityGet()]));
57
+ this.app.get.apply(this.app, processArgs(this.options, [resourceName + formName + id, this.entityGet()])); // We don't use the form name, but it can optionally be included so it can be referenced by the permissions check
58
+ this.app.post.apply(this.app, processArgs(this.options, [resourceName + id, this.entityPut()])); // You can POST or PUT to update data
59
+ this.app.put.apply(this.app, processArgs(this.options, [resourceName + id, this.entityPut()]));
60
+ this.app.delete.apply(this.app, processArgs(this.options, [resourceName + id, this.entityDelete()]));
61
+ this.app.get.apply(this.app, processArgs(this.options, ['search', this.searchAll()]));
62
+ for (let pluginName in this.options.plugins) {
63
+ if (this.options.plugins.hasOwnProperty(pluginName)) {
64
+ let pluginObj = this.options.plugins[pluginName];
65
+ this.options.plugins[pluginName] = Object.assign(this.options.plugins[pluginName], pluginObj.plugin(this, processArgs, pluginObj.options));
66
+ }
67
+ }
45
68
  }
46
- };
47
- module.exports = exports = DataForm;
48
- DataForm.prototype.extractTimestampFromMongoID = function (record) {
49
- var timestamp = record.toString().substring(0, 8);
50
- return new Date(parseInt(timestamp, 16) * 1000);
51
- };
52
- DataForm.prototype.getListFields = function (resource, doc, cb) {
53
- function getFirstMatchingField(keyList, type) {
54
- for (var i = 0; i < keyList.length; i++) {
55
- var fieldDetails = resource.model.schema['tree'][keyList[i]];
69
+ getFirstMatchingField(resource, doc, keyList, type) {
70
+ for (let i = 0; i < keyList.length; i++) {
71
+ let fieldDetails = resource.model.schema['tree'][keyList[i]];
56
72
  if (fieldDetails.type && (!type || fieldDetails.type.name === type) && keyList[i] !== '_id') {
57
73
  resource.options.listFields = [{ field: keyList[i] }];
58
- return doc[keyList[i]];
74
+ return doc ? doc[keyList[i]] : keyList[i];
59
75
  }
60
76
  }
61
77
  }
62
- var that = this;
63
- var display = '';
64
- var listFields = resource.options.listFields;
65
- if (listFields) {
66
- async.map(listFields, function (aField, cbm) {
67
- if (typeof doc[aField.field] !== 'undefined') {
68
- if (aField.params) {
69
- if (aField.params.ref) {
70
- var lookupResource_1 = that.getResource(resource.model.schema['paths'][aField.field].options.ref);
71
- if (lookupResource_1) {
72
- var hiddenFields = that.generateHiddenFields(lookupResource_1, false);
73
- hiddenFields.__v = 0;
74
- lookupResource_1.model.findOne({ _id: doc[aField.field] }).select(hiddenFields).exec(function (err, doc2) {
75
- if (err) {
76
- cbm(err);
77
- }
78
- else {
79
- that.getListFields(lookupResource_1, doc2, cbm);
78
+ getListFields(resource, doc, cb) {
79
+ const that = this;
80
+ let display = '';
81
+ let listFields = resource.options.listFields;
82
+ if (listFields) {
83
+ async.map(listFields, function (aField, cbm) {
84
+ if (typeof doc[aField.field] !== 'undefined') {
85
+ if (aField.params) {
86
+ if (aField.params.ref) {
87
+ let fieldOptions = resource.model.schema['paths'][aField.field].options;
88
+ if (typeof fieldOptions.ref === 'string') {
89
+ let lookupResource = that.getResource(fieldOptions.ref);
90
+ if (lookupResource) {
91
+ let hiddenFields = that.generateHiddenFields(lookupResource, false);
92
+ hiddenFields.__v = false;
93
+ lookupResource.model.findOne({ _id: doc[aField.field] }).select(hiddenFields).exec(function (err, doc2) {
94
+ if (err) {
95
+ cbm(err);
96
+ }
97
+ else {
98
+ that.getListFields(lookupResource, doc2, cbm);
99
+ }
100
+ });
80
101
  }
81
- });
102
+ }
103
+ else {
104
+ throw new Error('No support for ref type ' + aField.params.ref.type);
105
+ }
106
+ }
107
+ else if (aField.params.params === 'timestamp') {
108
+ let date = that.extractTimestampFromMongoID(doc[aField.field]);
109
+ cbm(null, date.toLocaleDateString() + ' ' + date.toLocaleTimeString());
82
110
  }
83
111
  }
84
- else if (aField.params.params === 'timestamp') {
85
- var date = this.extractTimestampFromMongoID(doc[aField.field]);
86
- cbm(null, date.toLocaleDateString() + ' ' + date.toLocaleTimeString());
112
+ else {
113
+ cbm(null, doc[aField.field]);
87
114
  }
88
115
  }
89
116
  else {
90
- cbm(null, doc[aField.field]);
117
+ cbm(null, '');
91
118
  }
92
- }
93
- else {
94
- cbm(null, '');
95
- }
96
- }, function (err, results) {
97
- if (err) {
98
- cb(err);
99
- }
100
- else {
101
- if (results) {
102
- cb(err, results.join(' ').trim());
119
+ }, function (err, results) {
120
+ if (err) {
121
+ cb(err);
103
122
  }
104
123
  else {
105
- console.log('No results ' + listFields);
124
+ if (results) {
125
+ cb(err, results.join(' ').trim());
126
+ }
127
+ else {
128
+ console.log('No results ' + listFields);
129
+ }
106
130
  }
107
- }
108
- });
109
- }
110
- else {
111
- var keyList = Object.keys(resource.model.schema['tree']);
112
- // No list field specified - use the first String field,
113
- display = getFirstMatchingField(keyList, 'String') ||
114
- // and if there aren't any then just take the first field
115
- getFirstMatchingField(keyList);
116
- cb(null, display.trim());
117
- }
118
- };
119
- /**
120
- * Registers all REST routes with the provided `app` object.
121
- */
122
- DataForm.prototype.registerRoutes = function () {
123
- var search = 'search/', schema = 'schema/', report = 'report/', resourceName = ':resourceName', id = '/:id';
124
- this.app.get.apply(this.app, processArgs(this.options, ['models', this.models()]));
125
- this.app.get.apply(this.app, processArgs(this.options, [search + resourceName, this.search()]));
126
- this.app.get.apply(this.app, processArgs(this.options, [schema + resourceName, this.schema()]));
127
- this.app.get.apply(this.app, processArgs(this.options, [schema + resourceName + '/:formName', this.schema()]));
128
- this.app.get.apply(this.app, processArgs(this.options, [report + resourceName, this.report()]));
129
- this.app.get.apply(this.app, processArgs(this.options, [report + resourceName + '/:reportName', this.report()]));
130
- this.app.get.apply(this.app, processArgs(this.options, [resourceName, this.collectionGet()]));
131
- this.app.post.apply(this.app, processArgs(this.options, [resourceName, this.collectionPost()]));
132
- this.app.get.apply(this.app, processArgs(this.options, [resourceName + id, this.entityGet()]));
133
- this.app.post.apply(this.app, processArgs(this.options, [resourceName + id, this.entityPut()])); // You can POST or PUT to update data
134
- this.app.put.apply(this.app, processArgs(this.options, [resourceName + id, this.entityPut()]));
135
- this.app.delete.apply(this.app, processArgs(this.options, [resourceName + id, this.entityDelete()]));
136
- // return the List attributes for a record - used by select2
137
- this.app.get.apply(this.app, processArgs(this.options, [resourceName + id + '/list', this.entityList()]));
138
- };
139
- DataForm.prototype.newResource = function (model, options) {
140
- options = options || {};
141
- options.suppressDeprecatedMessage = true;
142
- var passModel = model;
143
- if (typeof model !== 'function') {
144
- passModel = model.model;
145
- }
146
- this.addResource(passModel.modelName, passModel, options);
147
- };
148
- // Add a resource, specifying the model and any options.
149
- // Models may include their own options, which means they can be passed through from the model file
150
- DataForm.prototype.addResource = function (resourceName, model, options) {
151
- var resource = {
152
- resourceName: resourceName,
153
- options: options || {}
154
- };
155
- if (!resource.options.suppressDeprecatedMessage) {
156
- console.log('addResource is deprecated - see https://github.com/forms-angular/forms-angular/issues/39');
157
- }
158
- if (typeof model === 'function') {
159
- resource.model = model;
160
- }
161
- else {
162
- resource.model = model.model;
163
- for (var prop in model) {
164
- if (model.hasOwnProperty(prop) && prop !== 'model') {
165
- resource.options[prop] = model[prop];
166
- }
167
- }
168
- }
169
- extend(resource.options, this.preprocess(resource, resource.model.schema['paths'], null));
170
- if (resource.options.searchImportance) {
171
- this.searchFunc = async.forEachSeries;
172
- }
173
- if (this.searchFunc === async.forEachSeries) {
174
- this.resources.splice(_.sortedIndexBy(this.resources, resource, function (obj) {
175
- return obj.options.searchImportance || 99;
176
- }), 0, resource);
177
- }
178
- else {
179
- this.resources.push(resource);
180
- }
181
- };
182
- DataForm.prototype.getResource = function (name) {
183
- return _.find(this.resources, function (resource) {
184
- return resource.resourceName === name;
185
- });
186
- };
187
- DataForm.prototype.getResourceFromCollection = function (name) {
188
- return _.find(this.resources, function (resource) {
189
- return resource.model.collection.collectionName === name;
190
- });
191
- };
192
- DataForm.prototype.internalSearch = function (req, resourcesToSearch, includeResourceInResults, limit, callback) {
193
- if (typeof req.query === 'undefined') {
194
- req.query = {};
195
- }
196
- var searches = [], resourceCount = resourcesToSearch.length, searchFor = req.query.q || '', filter = req.query.f;
197
- function translate(string, array, context) {
198
- if (array) {
199
- var translation = _.find(array, function (fromTo) {
200
- return fromTo.from === string && (!fromTo.context || fromTo.context === context);
201
131
  });
202
- if (translation) {
203
- string = translation.to;
204
- }
205
132
  }
206
- return string;
207
- }
208
- // return a string that determines the sort order of the resultObject
209
- function calcResultValue(obj) {
210
- function padLeft(score, reqLength, str) {
211
- if (str === void 0) { str = '0'; }
212
- return new Array(1 + reqLength - String(score).length).join(str) + score;
133
+ else {
134
+ const keyList = Object.keys(resource.model.schema['tree']);
135
+ // No list field specified - use the first String field,
136
+ display = this.getFirstMatchingField(resource, doc, keyList, 'String') ||
137
+ // and if there aren't any then just take the first field
138
+ this.getFirstMatchingField(resource, doc, keyList);
139
+ cb(null, display.trim());
213
140
  }
214
- var sortString = '';
215
- sortString += padLeft(obj.addHits || 9, 1);
216
- sortString += padLeft(obj.searchImportance || 99, 2);
217
- sortString += padLeft(obj.weighting || 9999, 4);
218
- sortString += obj.text;
219
- return sortString;
220
- }
221
- if (filter) {
222
- filter = JSON.parse(filter);
223
141
  }
224
- for (var i = 0; i < resourceCount; i++) {
225
- var resource = resourcesToSearch[i];
226
- if (resourceCount === 1 || resource.options.searchImportance !== false) {
227
- var schema = resource.model.schema;
228
- var indexedFields = [];
229
- for (var j = 0; j < schema._indexes.length; j++) {
230
- var attributes = schema._indexes[j][0];
231
- var field = Object.keys(attributes)[0];
232
- if (indexedFields.indexOf(field) === -1) {
233
- indexedFields.push(field);
234
- }
235
- }
236
- for (var path in schema.paths) {
237
- if (path !== '_id' && schema.paths.hasOwnProperty(path)) {
238
- if (schema.paths[path]._index && !schema.paths[path].options.noSearch) {
239
- if (indexedFields.indexOf(path) === -1) {
240
- indexedFields.push(path);
241
- }
242
- }
142
+ ;
143
+ // generate a Mongo projection that can be used to restrict a query to return only those fields from the given
144
+ // resource that are identified as "list" fields (i.e., ones that should appear whenever records of that type are
145
+ // displayed in a list)
146
+ generateListFieldProjection(resource) {
147
+ const projection = {};
148
+ const listFields = resource.options?.listFields;
149
+ // resource.options.listFields will identify all of the fields from resource that have a value for .list.
150
+ // generally, that value will be "true", identifying the corresponding field as one which should be
151
+ // included whenever records of that type appear in a list.
152
+ // occasionally, it will instead be "{ ref: true }"", which means something entirely different -
153
+ // this means that the field requires a lookup translation before it can be displayed on a form.
154
+ // for our purposes, we're interested in only the first of these two cases, so we'll ignore anything where
155
+ // field.params.ref has a truthy value
156
+ if (listFields) {
157
+ for (const field of listFields) {
158
+ if (!field.params?.ref) {
159
+ projection[field.field] = 1;
243
160
  }
244
161
  }
245
- if (indexedFields.length === 0) {
246
- console.log('ERROR: Searching on a collection with no indexes ' + resource.resourceName);
247
- }
248
- for (var m = 0; m < indexedFields.length; m++) {
249
- searches.push({ resource: resource, field: indexedFields[m] });
250
- }
251
- }
252
- }
253
- var that = this;
254
- var results = [], moreCount = 0, searchCriteria, multiMatchPossible = searchFor.includes(' '), modifiedSearchStr = multiMatchPossible ? searchFor.split(' ').join('|') : searchFor;
255
- // Removed the logic that preserved spaces when collection was specified because Louise asked me to.
256
- searchCriteria = { $regex: '^(' + modifiedSearchStr + ')', $options: 'i' };
257
- this.searchFunc(searches, function (item, cb) {
258
- var searchDoc = {};
259
- if (filter) {
260
- extend(searchDoc, filter);
261
- if (filter[item.field]) {
262
- delete searchDoc[item.field];
263
- var obj1 = {}, obj2 = {};
264
- obj1[item.field] = filter[item.field];
265
- obj2[item.field] = searchCriteria;
266
- searchDoc['$and'] = [obj1, obj2];
267
- }
268
- else {
269
- searchDoc[item.field] = searchCriteria;
270
- }
271
162
  }
272
163
  else {
273
- searchDoc[item.field] = searchCriteria;
274
- }
275
- // The +60 in the next line is an arbitrary safety zone for situations where items that match the string
276
- // in more than one index get filtered out.
277
- // TODO : Figure out a better way to deal with this
278
- that.filteredFind(item.resource, req, null, searchDoc, item.resource.options.searchOrder, limit + 60, null, function (err, docs) {
279
- if (!err && docs && docs.length > 0) {
280
- async.map(docs, function (aDoc, cbdoc) {
281
- // Do we already have them in the list?
282
- var thisId = aDoc._id.toString(), resultObject, resultPos;
283
- function handleResultsInList() {
284
- resultObject.searchImportance = item.resource.options.searchImportance || 99;
285
- if (item.resource.options.localisationData) {
286
- resultObject.resource = translate(resultObject.resource, item.resource.options.localisationData, 'resource');
287
- resultObject.resourceText = translate(resultObject.resourceText, item.resource.options.localisationData, 'resourceText');
288
- resultObject.resourceTab = translate(resultObject.resourceTab, item.resource.options.localisationData, 'resourceTab');
289
- }
290
- results.splice(_.sortedIndexBy(results, resultObject, calcResultValue), 0, resultObject);
291
- cbdoc(null);
292
- }
293
- for (resultPos = results.length - 1; resultPos >= 0; resultPos--) {
294
- if (results[resultPos].id.toString() === thisId) {
295
- break;
296
- }
297
- }
298
- if (resultPos >= 0) {
299
- resultObject = {};
300
- extend(resultObject, results[resultPos]);
301
- // If they have already matched then improve their weighting
302
- // TODO: if the search string is B F currently Benjamin Barker scores same as Benjamin Franklin)
303
- if (multiMatchPossible) {
304
- resultObject.addHits = Math.max((resultObject.addHits || 9) - 1, 1);
305
- }
306
- // remove it from current position
307
- results.splice(resultPos, 1);
308
- // and re-insert where appropriate
309
- results.splice(_.sortedIndexBy(results, resultObject, calcResultValue), 0, resultObject);
310
- cbdoc(null);
311
- }
312
- else {
313
- // Otherwise add them new...
314
- // Use special listings format if defined
315
- var specialListingFormat = item.resource.options.searchResultFormat;
316
- if (specialListingFormat) {
317
- resultObject = specialListingFormat.apply(aDoc);
318
- handleResultsInList();
319
- }
320
- else {
321
- that.getListFields(item.resource, aDoc, function (err, description) {
322
- if (err) {
323
- cbdoc(err);
324
- }
325
- else {
326
- resultObject = {
327
- id: aDoc._id,
328
- weighting: 9999,
329
- text: description
330
- };
331
- if (resourceCount > 1 || includeResourceInResults) {
332
- resultObject.resource = resultObject.resourceText = item.resource.resourceName;
333
- }
334
- handleResultsInList();
335
- }
336
- });
337
- }
338
- }
339
- }, function (err) {
340
- cb(err);
341
- });
342
- }
343
- else {
344
- cb(err);
345
- }
346
- });
347
- }, function () {
348
- // Strip weighting from the results
349
- results = _.map(results, function (aResult) {
350
- delete aResult.weighting;
351
- return aResult;
352
- });
353
- if (results.length > limit) {
354
- moreCount += results.length - limit;
355
- results.splice(limit);
356
- }
357
- callback({ results: results, moreCount: moreCount });
358
- });
359
- };
360
- DataForm.prototype.search = function () {
361
- return _.bind(function (req, res, next) {
362
- if (!(req.resource = this.getResource(req.params.resourceName))) {
363
- return next();
164
+ const keyList = Object.keys(resource.model.schema['tree']);
165
+ const firstField = (
166
+ // No list field specified - use the first String field,
167
+ this.getFirstMatchingField(resource, undefined, keyList, 'String') ||
168
+ // and if there aren't any then just take the first field
169
+ this.getFirstMatchingField(resource, undefined, keyList));
170
+ projection[firstField] = 1;
364
171
  }
365
- this.internalSearch(req, [req.resource], false, 10, function (resultsObject) {
366
- res.send(resultsObject);
367
- });
368
- }, this);
369
- };
370
- DataForm.prototype.searchAll = function () {
371
- return _.bind(function (req, res) {
372
- this.internalSearch(req, this.resources, true, 10, function (resultsObject) {
373
- res.send(resultsObject);
374
- });
375
- }, this);
376
- };
377
- DataForm.prototype.models = function () {
378
- var that = this;
379
- return function (req, res) {
380
- // TODO: Make this less wasteful - we only need to send the resourceNames of the resources
381
- // Check for optional modelFilter and call it with the request and current list. Otherwise just return the list.
382
- res.send(that.options.modelFilter ? that.options.modelFilter.call(null, req, that.resources) : that.resources);
383
- };
384
- };
385
- DataForm.prototype.renderError = function (err, redirectUrl, req, res) {
386
- if (typeof err === 'string') {
387
- res.send(err);
172
+ return projection;
388
173
  }
389
- else {
390
- res.send(err.message);
174
+ ;
175
+ newResource(model, options) {
176
+ options = options || {};
177
+ options.suppressDeprecatedMessage = true;
178
+ let passModel = model;
179
+ if (typeof model !== 'function') {
180
+ passModel = model.model;
181
+ }
182
+ this.addResource(passModel.modelName, passModel, options);
391
183
  }
392
- };
393
- DataForm.prototype.redirect = function (address, req, res) {
394
- res.send(address);
395
- };
396
- DataForm.prototype.applySchemaSubset = function (vanilla, schema) {
397
- var outPath;
398
- if (schema) {
399
- outPath = {};
400
- for (var fld in schema) {
401
- if (schema.hasOwnProperty(fld)) {
402
- if (!vanilla[fld]) {
403
- throw new Error('No such field as ' + fld + '. Is it part of a sub-doc? If so you need the bit before the period.');
404
- }
405
- outPath[fld] = vanilla[fld];
406
- if (vanilla[fld].schema) {
407
- outPath[fld].schema = this.applySchemaSubset(outPath[fld].schema, schema[fld].schema);
408
- }
409
- outPath[fld].options = outPath[fld].options || {};
410
- for (var override in schema[fld]) {
411
- if (schema[fld].hasOwnProperty(override)) {
412
- if (!outPath[fld].options.form) {
413
- outPath[fld].options.form = {};
414
- }
415
- outPath[fld].options.form[override] = schema[fld][override];
416
- }
184
+ ;
185
+ // Add a resource, specifying the model and any options.
186
+ // Models may include their own options, which means they can be passed through from the model file
187
+ addResource(resourceName, model, options) {
188
+ let resource = {
189
+ resourceName: resourceName,
190
+ options: options || {}
191
+ };
192
+ if (!resource.options.suppressDeprecatedMessage) {
193
+ console.log('addResource is deprecated - see https://github.com/forms-angular/forms-angular/issues/39');
194
+ }
195
+ if (typeof model === 'function') {
196
+ resource.model = model;
197
+ }
198
+ else {
199
+ resource.model = model.model;
200
+ for (const prop in model) {
201
+ if (model.hasOwnProperty(prop) && prop !== 'model') {
202
+ resource.options[prop] = model[prop];
417
203
  }
418
204
  }
419
205
  }
206
+ extend(resource.options, this.preprocess(resource, resource.model.schema['paths'], null));
207
+ if (resource.options.searchImportance) {
208
+ this.searchFunc = async.forEachSeries;
209
+ }
210
+ if (this.searchFunc === async.forEachSeries) {
211
+ this.resources.splice(_.sortedIndexBy(this.resources, resource, function (obj) {
212
+ return obj.options.searchImportance || 99;
213
+ }), 0, resource);
214
+ }
215
+ else {
216
+ this.resources.push(resource);
217
+ }
420
218
  }
421
- else {
422
- outPath = vanilla;
219
+ ;
220
+ getResource(name) {
221
+ return _.find(this.resources, function (resource) {
222
+ return resource.resourceName === name;
223
+ });
423
224
  }
424
- return outPath;
425
- };
426
- DataForm.prototype.preprocess = function (resource, paths, formSchema) {
427
- var outPath = {}, hiddenFields = [], listFields = [];
428
- if (resource && resource.options && resource.options.idIsList) {
429
- paths['_id'].options = paths['_id'].options || {};
430
- paths['_id'].options.list = resource.options.idIsList;
225
+ ;
226
+ getResourceFromCollection(name) {
227
+ return _.find(this.resources, function (resource) {
228
+ return resource.model.collection.collectionName === name;
229
+ });
431
230
  }
432
- for (var element in paths) {
433
- if (paths.hasOwnProperty(element) && element !== '__v') {
434
- // check for schemas
435
- if (paths[element].schema) {
436
- var subSchemaInfo = this.preprocess(null, paths[element].schema.paths);
437
- outPath[element] = { schema: subSchemaInfo.paths };
438
- if (paths[element].options.form) {
439
- outPath[element].options = { form: extend(true, {}, paths[element].options.form) };
440
- }
441
- }
442
- else {
443
- // check for arrays
444
- var realType = paths[element].caster ? paths[element].caster : paths[element];
445
- if (!realType.instance) {
446
- if (realType.options.type) {
447
- var type = realType.options.type(), typeType = typeof type;
448
- if (typeType === 'string') {
449
- realType.instance = (!isNaN(Date.parse(type))) ? 'Date' : 'String';
231
+ ;
232
+ // Using the given (already-populated) AmbiguousRecordStore, generate text suitable for
233
+ // disambiguation of each ambiguous record, and pass that to the given disambiguateItemCallback
234
+ // so our caller can decorate the ambiguous record in whatever way it deems appropriate.
235
+ //
236
+ // The ambiguousRecordStore provided to this function (generated either by a call to
237
+ // buildSingleResourceAmbiguousRecordStore() or buildMultiResourceAmbiguousRecordStore()) will
238
+ // already be grouping records by the resource that should be used to disambiguate them, with
239
+ // the name of that resource being the primary index property of the store.
240
+ //
241
+ // The disambiguation text will be the concatenation (space-seperated) of the list fields for
242
+ // the doc from that resource whose _id matches the value of record[disambiguationField].
243
+ //
244
+ // allRecords should include all of the ambiguous records (also held by AmbiguousRecordStore)
245
+ // as well as those found not to be ambiguous. The final act of this function will be to delete
246
+ // the disambiguation field from those records - it is only going to be there for the purpose
247
+ // of disambiguation, and should not be returned by our caller once disambiguation is complete.
248
+ //
249
+ // The scary-looking templating used here ensures that the objects in allRecords (and also
250
+ // ambiguousRecordStore) include an (optional) string property with the name identified by
251
+ // disambiguationField. For the avoidance of doubt, "prop" here could be anything - "foo in f"
252
+ // would achieve the same result.
253
+ disambiguate(allRecords, ambiguousRecordStore, disambiguationField, disambiguateItemCallback, completionCallback) {
254
+ const that = this;
255
+ async.map(Object.keys(ambiguousRecordStore), function (resourceName, cbm) {
256
+ const resource = that.getResource(resourceName);
257
+ const projection = that.generateListFieldProjection(resource);
258
+ resource.model
259
+ .find({ _id: { $in: ambiguousRecordStore[resourceName].map((sr) => sr[disambiguationField]) } })
260
+ .select(projection)
261
+ .lean()
262
+ .then((disambiguationRecs) => {
263
+ for (const ambiguousResult of ambiguousRecordStore[resourceName]) {
264
+ const disambiguator = disambiguationRecs.find((d) => d._id.toString() === ambiguousResult[disambiguationField].toString());
265
+ if (disambiguator) {
266
+ let suffix = "";
267
+ for (const listField in projection) {
268
+ if (disambiguator[listField]) {
269
+ if (suffix) {
270
+ suffix += " ";
271
+ }
272
+ suffix += disambiguator[listField];
273
+ }
450
274
  }
451
- else {
452
- realType.instance = typeType;
275
+ if (suffix) {
276
+ disambiguateItemCallback(ambiguousResult, suffix);
453
277
  }
454
278
  }
455
279
  }
456
- outPath[element] = extend(true, {}, paths[element]);
457
- if (paths[element].options.secure) {
458
- hiddenFields.push(element);
459
- }
460
- if (paths[element].options.match) {
461
- outPath[element].options.match = paths[element].options.match.source;
462
- }
463
- var schemaListInfo = paths[element].options.list;
464
- if (schemaListInfo) {
465
- var listFieldInfo = { field: element };
466
- if (typeof schemaListInfo === 'object' && Object.keys(schemaListInfo).length > 0) {
467
- listFieldInfo.params = schemaListInfo;
468
- }
469
- listFields.push(listFieldInfo);
470
- }
280
+ cbm(null);
281
+ })
282
+ .catch((err) => {
283
+ cbm(err);
284
+ });
285
+ }, (err) => {
286
+ for (const record of allRecords) {
287
+ delete record[disambiguationField];
471
288
  }
472
- }
473
- }
474
- outPath = this.applySchemaSubset(outPath, formSchema);
475
- var returnObj = { paths: outPath };
476
- if (hiddenFields.length > 0) {
477
- returnObj.hide = hiddenFields;
478
- }
479
- if (listFields.length > 0) {
480
- returnObj.listFields = listFields;
289
+ completionCallback(err);
290
+ });
481
291
  }
482
- return returnObj;
483
- };
484
- DataForm.prototype.schema = function () {
485
- return _.bind(function (req, res) {
486
- if (!(req.resource = this.getResource(req.params.resourceName))) {
487
- return res.status(404).end();
488
- }
489
- var formSchema = null;
490
- if (req.params.formName) {
491
- formSchema = req.resource.model.schema.statics['form'](req.params.formName);
492
- }
493
- var paths = this.preprocess(req.resource, req.resource.model.schema.paths, formSchema).paths;
494
- res.send(paths);
495
- }, this);
496
- };
497
- DataForm.prototype.report = function () {
498
- return _.bind(function (req, res, next) {
499
- if (!(req.resource = this.getResource(req.params.resourceName))) {
500
- return next();
501
- }
502
- var self = this;
292
+ internalSearch(req, resourcesToSearch, includeResourceInResults, limit, callback) {
503
293
  if (typeof req.query === 'undefined') {
504
294
  req.query = {};
505
295
  }
506
- var reportSchema;
507
- if (req.params.reportName) {
508
- reportSchema = req.resource.model.schema.statics['report'](req.params.reportName, req);
296
+ const timestamps = { sentAt: req.query.sentAt, startedAt: new Date().valueOf(), completedAt: undefined };
297
+ let searches = [], resourceCount = resourcesToSearch.length, searchFor = req.query.q || '', filter = req.query.f;
298
+ function translate(string, array, context) {
299
+ if (array) {
300
+ let translation = _.find(array, function (fromTo) {
301
+ return fromTo.from === string && (!fromTo.context || fromTo.context === context);
302
+ });
303
+ if (translation) {
304
+ string = translation.to;
305
+ }
306
+ }
307
+ return string;
509
308
  }
510
- else if (req.query.r) {
511
- switch (req.query.r[0]) {
512
- case '[':
513
- reportSchema = { pipeline: JSON.parse(req.query.r) };
514
- break;
515
- case '{':
516
- reportSchema = JSON.parse(req.query.r);
517
- break;
518
- default:
519
- return self.renderError(new Error('Invalid "r" parameter'), null, req, res, next);
309
+ // return a string that determines the sort order of the resultObject
310
+ function calcResultValue(obj) {
311
+ function padLeft(score, reqLength, str = '0') {
312
+ return new Array(1 + reqLength - String(score).length).join(str) + score;
520
313
  }
314
+ let sortString = '';
315
+ sortString += padLeft(obj.addHits || 9, 1);
316
+ sortString += padLeft(obj.searchImportance || 99, 2);
317
+ sortString += padLeft(obj.weighting || 9999, 4);
318
+ sortString += obj.text;
319
+ return sortString;
521
320
  }
522
- else {
523
- var fields = {};
524
- for (var key in req.resource.model.schema.paths) {
525
- if (req.resource.model.schema.paths.hasOwnProperty(key)) {
526
- if (key !== '__v' && !req.resource.model.schema.paths[key].options.secure) {
527
- if (key.indexOf('.') === -1) {
528
- fields[key] = 1;
321
+ if (filter) {
322
+ filter = JSON.parse(filter);
323
+ }
324
+ // See if we are narrowing down the resources
325
+ let collectionName;
326
+ let collectionNameLower;
327
+ let colonPos = searchFor.indexOf(':');
328
+ switch (colonPos) {
329
+ case -1:
330
+ // Original behaviour = do nothing different
331
+ break;
332
+ case 0:
333
+ // "Special search" - yet to be implemented
334
+ break;
335
+ default:
336
+ collectionName = searchFor.slice(0, colonPos);
337
+ collectionNameLower = collectionName.toLowerCase();
338
+ searchFor = searchFor.slice(colonPos + 1, 999).trim();
339
+ if (searchFor === '') {
340
+ searchFor = '?';
341
+ }
342
+ break;
343
+ }
344
+ for (let i = 0; i < resourceCount; i++) {
345
+ let resource = resourcesToSearch[i];
346
+ if (resourceCount === 1 || (resource.options.searchImportance !== false && (!collectionName || collectionName === resource.resourceName || resource.options?.synonyms?.find(s => s.name?.toLowerCase() === collectionNameLower)))) {
347
+ let schema = resource.model.schema;
348
+ let indexedFields = [];
349
+ for (let j = 0; j < schema._indexes.length; j++) {
350
+ let attributes = schema._indexes[j][0];
351
+ let field = Object.keys(attributes)[0];
352
+ if (indexedFields.indexOf(field) === -1) {
353
+ indexedFields.push(field);
354
+ }
355
+ }
356
+ for (let path in schema.paths) {
357
+ if (path !== '_id' && schema.paths.hasOwnProperty(path)) {
358
+ if (schema.paths[path]._index && !schema.paths[path].options.noSearch) {
359
+ if (indexedFields.indexOf(path) === -1) {
360
+ indexedFields.push(path);
361
+ }
529
362
  }
530
363
  }
531
364
  }
365
+ if (indexedFields.length === 0) {
366
+ console.log('ERROR: Searching on a collection with no indexes ' + resource.resourceName);
367
+ }
368
+ let synonymObj = resource.options?.synonyms?.find(s => s.name.toLowerCase() === collectionNameLower);
369
+ const synonymFilter = synonymObj?.filter;
370
+ for (let m = 0; m < indexedFields.length; m++) {
371
+ let searchObj = { resource: resource, field: indexedFields[m] };
372
+ if (synonymFilter) {
373
+ searchObj.filter = synonymFilter;
374
+ }
375
+ searches.push(searchObj);
376
+ }
532
377
  }
533
- reportSchema = {
534
- pipeline: [
535
- { $project: fields }
536
- ], drilldown: req.params.resourceName + '/|_id|/edit'
537
- };
538
378
  }
539
- // Replace parameters in pipeline
540
- var schemaCopy = {};
541
- extend(schemaCopy, reportSchema);
542
- schemaCopy.params = schemaCopy.params || [];
543
- self.reportInternal(req, req.resource, schemaCopy, function (err, result) {
544
- if (err) {
545
- self.renderError(err, null, req, res, next);
379
+ const that = this;
380
+ let results = [];
381
+ let moreCount = 0;
382
+ let searchCriteria;
383
+ let searchStrings;
384
+ let multiMatchPossible = false;
385
+ if (searchFor === '?') {
386
+ // interpret this as a wildcard (so there is no way to search for ?
387
+ searchCriteria = null;
388
+ }
389
+ else {
390
+ // Support for searching anywhere in a field by starting with *
391
+ let startAnchor = '^';
392
+ if (searchFor.slice(0, 1) === '*') {
393
+ startAnchor = '';
394
+ searchFor = searchFor.slice(1);
546
395
  }
547
- else {
548
- res.send(result);
396
+ // THe snippet to escape the special characters comes from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
397
+ searchFor = searchFor.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
398
+ multiMatchPossible = searchFor.includes(' ');
399
+ if (multiMatchPossible) {
400
+ searchStrings = searchFor.split(' ');
549
401
  }
550
- });
551
- }, this);
552
- };
553
- DataForm.prototype.hackVariablesInPipeline = function (runPipeline) {
554
- for (var pipelineSection = 0; pipelineSection < runPipeline.length; pipelineSection++) {
555
- if (runPipeline[pipelineSection]['$match']) {
556
- this.hackVariables(runPipeline[pipelineSection]['$match']);
402
+ let modifiedSearchStr = multiMatchPossible ? searchStrings.join('|') : searchFor;
403
+ searchFor = searchFor.toLowerCase(); // For later case-insensitive comparison
404
+ // Removed the logic that preserved spaces when collection was specified because Louise asked me to.
405
+ searchCriteria = { $regex: `${startAnchor}(${modifiedSearchStr})`, $options: 'i' };
557
406
  }
558
- }
559
- };
560
- DataForm.prototype.hackVariables = function (obj) {
561
- // Replace variables that cannot be serialised / deserialised. Bit of a hack, but needs must...
562
- // Anything formatted 1800-01-01T00:00:00.000Z or 1800-01-01T00:00:00.000+0000 is converted to a Date
563
- // Only handles the cases I need for now
564
- // TODO: handle arrays etc
565
- for (var prop in obj) {
566
- if (obj.hasOwnProperty(prop)) {
567
- if (typeof obj[prop] === 'string') {
568
- var dateTest = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3})(Z|[+ -]\d{4})$/.exec(obj[prop]);
569
- if (dateTest) {
570
- obj[prop] = new Date(dateTest[1] + 'Z');
407
+ let handleSearchResultsFromIndex = function (err, docs, item, cb) {
408
+ function handleSingleSearchResult(aDoc, cbdoc) {
409
+ let thisId = aDoc._id.toString(), resultObject, resultPos;
410
+ function handleResultsInList() {
411
+ if (multiMatchPossible) {
412
+ resultObject.matched = resultObject.matched || [];
413
+ // record the index of string that matched, so we don't count it against another field
414
+ for (let i = 0; i < searchStrings.length; i++) {
415
+ if (aDoc[item.field].toLowerCase().indexOf(searchStrings[i]) === 0) {
416
+ resultObject.matched.push(i);
417
+ break;
418
+ }
419
+ }
420
+ }
421
+ resultObject.searchImportance = item.resource.options.searchImportance || 99;
422
+ if (item.resource.options.localisationData) {
423
+ resultObject.resource = translate(resultObject.resource, item.resource.options.localisationData, "resource");
424
+ resultObject.resourceText = translate(resultObject.resourceText, item.resource.options.localisationData, "resourceText");
425
+ resultObject.resourceTab = translate(resultObject.resourceTab, item.resource.options.localisationData, "resourceTab");
426
+ }
427
+ results.splice(_.sortedIndexBy(results, resultObject, calcResultValue), 0, resultObject);
428
+ cbdoc(null);
571
429
  }
572
- else {
573
- var objectIdTest = /^([0-9a-fA-F]{24})$/.exec(obj[prop]);
574
- if (objectIdTest) {
575
- obj[prop] = new this.mongoose.Schema.Types.ObjectId(objectIdTest[1]);
430
+ // Do we already have them in the list?
431
+ for (resultPos = results.length - 1; resultPos >= 0; resultPos--) {
432
+ if (results[resultPos].id.toString() === thisId) {
433
+ break;
576
434
  }
577
435
  }
578
- }
579
- else if (_.isObject(obj[prop])) {
580
- this.hackVariables(obj[prop]);
581
- }
582
- }
583
- }
584
- };
585
- DataForm.prototype.reportInternal = function (req, resource, schema, callback) {
586
- var runPipeline;
587
- var self = this;
588
- if (typeof req.query === 'undefined') {
589
- req.query = {};
590
- }
591
- self.doFindFunc(req, resource, function (err, queryObj) {
592
- if (err) {
593
- return 'There was a problem with the findFunc for model';
594
- }
595
- else {
596
- // Bit crap here switching back and forth to string
597
- runPipeline = JSON.stringify(schema.pipeline);
598
- for (var param in req.query) {
599
- if (req.query[param]) {
600
- if (param !== 'r') { // we don't want to copy the whole report schema (again!)
601
- schema.params[param].value = req.query[param];
436
+ if (resultPos >= 0) {
437
+ resultObject = Object.assign({}, results[resultPos]);
438
+ // If they have already matched then improve their weighting
439
+ if (multiMatchPossible) {
440
+ // record the index of string that matched, so we don't count it against another field
441
+ for (let i = 0; i < searchStrings.length; i++) {
442
+ if (!resultObject.matched.includes(i) && aDoc[item.field].toLowerCase().indexOf(searchStrings[i]) === 0) {
443
+ resultObject.matched.push(i);
444
+ resultObject.addHits = Math.max((resultObject.addHits || 9) - 1, 0);
445
+ // remove it from current position
446
+ results.splice(resultPos, 1);
447
+ // and re-insert where appropriate
448
+ results.splice(_.sortedIndexBy(results, resultObject, calcResultValue), 0, resultObject);
449
+ break;
450
+ }
451
+ }
602
452
  }
453
+ cbdoc(null);
603
454
  }
604
- }
605
- // Replace parameters with the value
606
- if (runPipeline) {
607
- runPipeline = runPipeline.replace(/\"\(.+?\)\"/g, function (match) {
608
- var sparam = schema.params[match.slice(2, -2)];
609
- if (sparam.type === 'number') {
610
- return sparam.value;
455
+ else {
456
+ // Otherwise add them new...
457
+ let addHits;
458
+ if (multiMatchPossible) {
459
+ // If they match the whole search phrase in one index they get smaller addHits (so they sort higher)
460
+ if (aDoc[item.field].toLowerCase().indexOf(searchFor) === 0) {
461
+ addHits = 7;
462
+ }
611
463
  }
612
- else if (_.isObject(sparam.value)) {
613
- return JSON.stringify(sparam.value);
464
+ let disambiguationId;
465
+ const opts = item.resource.options;
466
+ const disambiguationResource = opts.disambiguation?.resource;
467
+ if (disambiguationResource) {
468
+ disambiguationId = aDoc[opts.disambiguation.field]?.toString();
614
469
  }
615
- else if (sparam.value[0] === '{') {
616
- return sparam.value;
470
+ // Use special listings format if defined
471
+ let specialListingFormat = item.resource.options.searchResultFormat;
472
+ if (specialListingFormat) {
473
+ specialListingFormat.apply(aDoc, [req])
474
+ .then((resultObj) => {
475
+ resultObject = resultObj;
476
+ resultObject.addHits = addHits;
477
+ resultObject.disambiguationResource = disambiguationResource;
478
+ resultObject.disambiguationId = disambiguationId;
479
+ handleResultsInList();
480
+ });
617
481
  }
618
482
  else {
619
- return '"' + sparam.value + '"';
620
- }
621
- });
622
- }
623
- // Don't send the 'secure' fields
624
- var hiddenFields = self.generateHiddenFields(resource, false);
625
- for (var hiddenField in hiddenFields) {
626
- if (hiddenFields.hasOwnProperty(hiddenField)) {
627
- if (runPipeline.indexOf(hiddenField) !== -1) {
628
- return callback('You cannot access ' + hiddenField);
629
- }
630
- }
631
- }
632
- runPipeline = JSON.parse(runPipeline);
633
- self.hackVariablesInPipeline(runPipeline);
634
- // Add the findFunc query to the pipeline
635
- if (queryObj) {
636
- runPipeline.unshift({ $match: queryObj });
637
- }
638
- var toDo = {
639
- runAggregation: function (cb) {
640
- resource.model.aggregate(runPipeline, cb);
641
- }
642
- };
643
- var translations_1 = []; // array of form {ref:'lookupname',translations:[{value:xx, display:' '}]}
644
- // if we need to do any column translations add the function to the tasks list
645
- if (schema.columnTranslations) {
646
- toDo.applyTranslations = ['runAggregation', function (results, cb) {
647
- function doATranslate(column, theTranslation) {
648
- results['runAggregation'].forEach(function (resultRow) {
649
- var valToTranslate = resultRow[column.field];
650
- valToTranslate = (valToTranslate ? valToTranslate.toString() : '');
651
- var thisTranslation = _.find(theTranslation.translations, function (option) {
652
- return valToTranslate === option.value.toString();
653
- });
654
- resultRow[column.field] = thisTranslation ? thisTranslation.display : ' * Missing columnTranslation * ';
655
- });
656
- }
657
- schema.columnTranslations.forEach(function (columnTranslation) {
658
- if (columnTranslation.translations) {
659
- doATranslate(columnTranslation, columnTranslation);
483
+ that.getListFields(item.resource, aDoc, function (err, description) {
484
+ if (err) {
485
+ cbdoc(err);
660
486
  }
661
- if (columnTranslation.ref) {
662
- var theTranslation = _.find(translations_1, function (translation) {
663
- return (translation.ref === columnTranslation.ref);
664
- });
665
- if (theTranslation) {
666
- doATranslate(columnTranslation, theTranslation);
667
- }
668
- else {
669
- cb('Invalid ref property of ' + columnTranslation.ref + ' in columnTranslations ' + columnTranslation.field);
487
+ else {
488
+ resultObject = {
489
+ id: aDoc._id,
490
+ weighting: 9999,
491
+ addHits,
492
+ disambiguationResource,
493
+ disambiguationId,
494
+ text: description
495
+ };
496
+ if (resourceCount > 1 || includeResourceInResults) {
497
+ resultObject.resource = resultObject.resourceText = item.resource.resourceName;
670
498
  }
499
+ handleResultsInList();
671
500
  }
672
501
  });
673
- cb(null, null);
674
- }];
675
- var callFuncs = false;
676
- for (var i = 0; i < schema.columnTranslations.length; i++) {
677
- var thisColumnTranslation = schema.columnTranslations[i];
678
- if (thisColumnTranslation.field) {
679
- // if any of the column translations are adhoc funcs, set up the tasks to perform them
680
- if (thisColumnTranslation.fn) {
681
- callFuncs = true;
682
- }
683
- // if this column translation is a "ref", set up the tasks to look up the values and populate the translations
684
- if (thisColumnTranslation.ref) {
685
- var lookup = self.getResource(thisColumnTranslation.ref);
686
- if (lookup) {
687
- if (!toDo[thisColumnTranslation.ref]) {
688
- var getFunc = function (ref) {
689
- var lookup = ref;
690
- return function (cb) {
691
- var translateObject = { ref: lookup.resourceName, translations: [] };
692
- translations_1.push(translateObject);
693
- lookup.model.find({}, {}, { lean: true }, function (err, findResults) {
694
- if (err) {
695
- cb(err);
696
- }
697
- else {
698
- // TODO - this ref func can probably be done away with now that list fields can have ref
699
- var j_1 = 0;
700
- async.whilst(function () {
701
- return j_1 < findResults.length;
702
- }, function (cbres) {
703
- var theResult = findResults[j_1];
704
- translateObject.translations[j_1] = translateObject.translations[j_1] || {};
705
- var theTranslation = translateObject.translations[j_1];
706
- j_1++;
707
- self.getListFields(lookup, theResult, function (err, description) {
708
- if (err) {
709
- cbres(err);
710
- }
711
- else {
712
- theTranslation.value = theResult._id;
713
- theTranslation.display = description;
714
- cbres(null);
715
- }
716
- });
717
- }, cb);
718
- }
719
- });
720
- };
721
- };
722
- toDo[thisColumnTranslation.ref] = getFunc(lookup);
723
- toDo.applyTranslations.unshift(thisColumnTranslation.ref); // Make sure we populate lookup before doing translation
724
- }
725
- }
726
- else {
727
- return callback('Invalid ref property of ' + thisColumnTranslation.ref + ' in columnTranslations ' + thisColumnTranslation.field);
728
- }
729
- }
730
- if (!thisColumnTranslation.translations && !thisColumnTranslation.ref && !thisColumnTranslation.fn) {
731
- return callback('A column translation needs a ref, fn or a translations property - ' + thisColumnTranslation.field + ' has neither');
732
- }
733
- }
734
- else {
735
- return callback('A column translation needs a field property');
736
502
  }
737
503
  }
738
- if (callFuncs) {
739
- toDo['callFunctions'] = ['runAggregation', function (results, cb) {
740
- async.each(results.runAggregation, function (row, cb) {
741
- for (var i = 0; i < schema.columnTranslations.length; i++) {
742
- var thisColumnTranslation = schema.columnTranslations[i];
743
- if (thisColumnTranslation.fn) {
744
- thisColumnTranslation.fn(row, cb);
745
- }
746
- }
747
- }, function () {
748
- cb(null);
749
- });
750
- }];
751
- toDo.applyTranslations.unshift('callFunctions'); // Make sure we do function before translating its result
752
- }
753
504
  }
754
- async.auto(toDo, function (err, results) {
755
- if (err) {
756
- callback(err);
757
- }
758
- else {
759
- // TODO: Could loop through schema.params and just send back the values
760
- callback(null, { success: true, schema: schema, report: results.runAggregation, paramsUsed: schema.params });
761
- }
762
- });
763
- }
764
- });
765
- };
766
- DataForm.prototype.saveAndRespond = function (req, res, hiddenFields) {
767
- function internalSave(doc) {
768
- doc.save(function (err, doc2) {
769
- if (err) {
770
- var err2 = { status: 'err' };
771
- if (!err.errors) {
772
- err2.message = err.message;
505
+ if (!err && docs && docs.length > 0) {
506
+ async.map(docs, handleSingleSearchResult, cb);
507
+ }
508
+ else {
509
+ cb(err);
510
+ }
511
+ };
512
+ this.searchFunc(searches, function (item, cb) {
513
+ let searchDoc = {};
514
+ let searchFilter = filter || item.filter;
515
+ if (searchFilter) {
516
+ that.hackVariables(searchFilter);
517
+ extend(searchDoc, searchFilter);
518
+ if (searchFilter[item.field]) {
519
+ delete searchDoc[item.field];
520
+ let obj1 = {}, obj2 = {};
521
+ obj1[item.field] = searchFilter[item.field];
522
+ obj2[item.field] = searchCriteria;
523
+ searchDoc['$and'] = [obj1, obj2];
773
524
  }
774
525
  else {
775
- extend(err2, err);
526
+ if (searchCriteria) {
527
+ searchDoc[item.field] = searchCriteria;
528
+ }
776
529
  }
777
- if (debug) {
778
- console.log('Error saving record: ' + JSON.stringify(err2));
530
+ }
531
+ else {
532
+ if (searchCriteria) {
533
+ searchDoc[item.field] = searchCriteria;
779
534
  }
780
- res.status(400).send(err2);
535
+ }
536
+ /*
537
+ The +200 below line is an (imperfect) arbitrary safety zone for situations where items that match the string in more than one index get filtered out.
538
+ An example where it fails is searching for "e c" which fails to get a old record Emily Carpenter in a big dataset sorted by date last accessed as they
539
+ are not returned within the first 200 in forenames so don't get the additional hit score and languish outside the visible results, though those visible
540
+ results end up containing people who only match either c or e (but have been accessed much more recently).
541
+
542
+ Increasing the number would be a short term fix at the cost of slowing down the search.
543
+ */
544
+ // TODO : Figure out a better way to deal with this
545
+ if (item.resource.options.searchFunc) {
546
+ item.resource.options.searchFunc(item.resource, req, null, searchDoc, item.resource.options.searchOrder, limit ? limit + 200 : 0, null, function (err, docs) {
547
+ handleSearchResultsFromIndex(err, docs, item, cb);
548
+ });
781
549
  }
782
550
  else {
783
- doc2 = doc2.toObject();
784
- for (var hiddenField in hiddenFields) {
785
- if (hiddenFields.hasOwnProperty(hiddenField) && hiddenFields[hiddenField]) {
786
- var parts = hiddenField.split('.');
787
- var lastPart = parts.length - 1;
788
- var target = doc2;
789
- for (var i = 0; i < lastPart; i++) {
790
- if (target.hasOwnProperty(parts[i])) {
791
- target = target[parts[i]];
792
- }
793
- }
794
- if (target.hasOwnProperty(parts[lastPart])) {
795
- delete target[parts[lastPart]];
551
+ that.filteredFind(item.resource, req, null, searchDoc, null, item.resource.options.searchOrder, limit ? limit + 200 : 0, null, function (err, docs) {
552
+ handleSearchResultsFromIndex(err, docs, item, cb);
553
+ });
554
+ }
555
+ }, function (err) {
556
+ if (err) {
557
+ callback(err);
558
+ }
559
+ else {
560
+ // Strip weighting from the results
561
+ results = _.map(results, function (aResult) {
562
+ delete aResult.weighting;
563
+ return aResult;
564
+ });
565
+ if (limit && results.length > limit) {
566
+ moreCount += results.length - limit;
567
+ results.splice(limit);
568
+ }
569
+ that.disambiguate(results, that.buildMultiResourceAmbiguousRecordStore(results, ["text"], "disambiguationResource"), "disambiguationId", (item, disambiguationText) => {
570
+ item.text += ` (${disambiguationText})`;
571
+ }, (err) => {
572
+ if (err) {
573
+ callback(err);
574
+ }
575
+ else {
576
+ // the disambiguate() call will have deleted the disambiguationIds but we're responsible for
577
+ // the disambiguationResources, which we shouldn't be returning to the client
578
+ for (const result of results) {
579
+ delete result.disambiguationResource;
796
580
  }
581
+ timestamps.completedAt = new Date().valueOf();
582
+ callback(null, { results, moreCount, timestamps });
797
583
  }
798
- }
799
- res.send(doc2);
584
+ });
800
585
  }
801
586
  });
802
587
  }
803
- var doc = req.doc;
804
- if (typeof req.resource.options.onSave === 'function') {
805
- req.resource.options.onSave(doc, req, function (err) {
588
+ ;
589
+ wrapInternalSearch(req, res, resourcesToSearch, includeResourceInResults, limit) {
590
+ this.internalSearch(req, resourcesToSearch, includeResourceInResults, limit, function (err, resultsObject) {
806
591
  if (err) {
807
- throw err;
592
+ res.status(400, err);
593
+ }
594
+ else {
595
+ res.send(resultsObject);
808
596
  }
809
- internalSave(doc);
810
597
  });
811
598
  }
812
- else {
813
- internalSave(doc);
814
- }
815
- };
816
- /**
817
- * All entities REST functions have to go through this first.
818
- */
819
- DataForm.prototype.processCollection = function (req) {
820
- req.resource = this.getResource(req.params.resourceName);
821
- };
822
- /**
823
- * Renders a view with the list of docs, which may be modified by query parameters
824
- */
825
- DataForm.prototype.collectionGet = function () {
826
- return _.bind(function (req, res, next) {
827
- this.processCollection(req);
828
- if (!req.resource) {
829
- return next();
830
- }
831
- if (typeof req.query === 'undefined') {
832
- req.query = {};
833
- }
834
- try {
835
- var aggregationParam = req.query.a ? JSON.parse(req.query.a) : null;
836
- var findParam = req.query.f ? JSON.parse(req.query.f) : {};
837
- var limitParam = req.query.l ? JSON.parse(req.query.l) : 0;
838
- var skipParam = req.query.s ? JSON.parse(req.query.s) : 0;
839
- var orderParam = req.query.o ? JSON.parse(req.query.o) : req.resource.options.listOrder;
840
- // Dates in aggregation must be Dates
841
- if (aggregationParam) {
842
- this.hackVariablesInPipeline(aggregationParam);
599
+ ;
600
+ search() {
601
+ return _.bind(function (req, res, next) {
602
+ if (!(req.resource = this.getResource(req.params.resourceName))) {
603
+ return next();
843
604
  }
844
- var self_1 = this;
845
- this.filteredFind(req.resource, req, aggregationParam, findParam, orderParam, limitParam, skipParam, function (err, docs) {
846
- if (err) {
847
- return self_1.renderError(err, null, req, res, next);
848
- }
849
- else {
850
- res.send(docs);
851
- }
852
- });
853
- }
854
- catch (e) {
855
- res.send(e);
856
- }
857
- }, this);
858
- };
859
- DataForm.prototype.doFindFunc = function (req, resource, cb) {
860
- if (resource.options.findFunc) {
861
- resource.options.findFunc(req, cb);
605
+ this.wrapInternalSearch(req, res, [req.resource], false, 0);
606
+ }, this);
862
607
  }
863
- else {
864
- cb(null);
608
+ ;
609
+ searchAll() {
610
+ return _.bind(function (req, res) {
611
+ this.wrapInternalSearch(req, res, this.resources, true, 10);
612
+ }, this);
865
613
  }
866
- };
867
- DataForm.prototype.filteredFind = function (resource, req, aggregationParam, findParam, sortOrder, limit, skip, callback) {
868
- var that = this;
869
- var hiddenFields = this.generateHiddenFields(resource, false);
870
- function doAggregation(cb) {
871
- if (aggregationParam) {
872
- resource.model.aggregate(aggregationParam, function (err, aggregationResults) {
873
- if (err) {
874
- throw err;
875
- }
876
- else {
877
- cb(_.map(aggregationResults, function (obj) {
878
- return obj._id;
879
- }));
614
+ ;
615
+ models() {
616
+ const that = this;
617
+ return function (req, res) {
618
+ // TODO: Make this less wasteful - we only need to send the resourceNames of the resources
619
+ // Check for optional modelFilter and call it with the request and current list. Otherwise just return the list.
620
+ res.send(that.options.modelFilter ? that.options.modelFilter.call(null, req, that.resources) : that.resources);
621
+ };
622
+ }
623
+ ;
624
+ renderError(err, redirectUrl, req, res) {
625
+ res.statusMessage = err?.message || err;
626
+ res.status(400).end(err?.message || err);
627
+ }
628
+ ;
629
+ redirect(address, req, res) {
630
+ res.send(address);
631
+ }
632
+ ;
633
+ applySchemaSubset(vanilla, schema) {
634
+ let outPath;
635
+ if (schema) {
636
+ outPath = {};
637
+ for (let fld in schema) {
638
+ if (schema.hasOwnProperty(fld)) {
639
+ if (vanilla[fld]) {
640
+ outPath[fld] = _.cloneDeep(vanilla[fld]);
641
+ if (vanilla[fld].schema) {
642
+ outPath[fld].schema = this.applySchemaSubset(outPath[fld].schema, schema[fld].schema);
643
+ }
644
+ }
645
+ else {
646
+ if (fld.slice(0, 8) === "_bespoke") {
647
+ outPath[fld] = {
648
+ "path": fld,
649
+ "instance": schema[fld]._type,
650
+ };
651
+ }
652
+ else {
653
+ throw new Error('No such field as ' + fld + '. Is it part of a sub-doc? If so you need the bit before the period.');
654
+ }
655
+ }
656
+ outPath[fld].options = outPath[fld].options || {};
657
+ for (const override in schema[fld]) {
658
+ if (schema[fld].hasOwnProperty(override)) {
659
+ if (override.slice(0, 1) !== '_') {
660
+ if (schema[fld].hasOwnProperty(override)) {
661
+ if (!outPath[fld].options.form) {
662
+ outPath[fld].options.form = {};
663
+ }
664
+ outPath[fld].options.form[override] = schema[fld][override];
665
+ }
666
+ }
667
+ }
668
+ }
880
669
  }
881
- });
670
+ }
882
671
  }
883
672
  else {
884
- cb([]);
673
+ outPath = vanilla;
885
674
  }
675
+ return outPath;
886
676
  }
887
- doAggregation(function (idArray) {
888
- if (aggregationParam && idArray.length === 0) {
889
- callback(null, []);
677
+ ;
678
+ preprocess(resource, paths, formSchema) {
679
+ let outPath = {}, hiddenFields = [], listFields = [];
680
+ if (resource && resource.options && resource.options.idIsList) {
681
+ paths['_id'].options = paths['_id'].options || {};
682
+ paths['_id'].options.list = resource.options.idIsList;
890
683
  }
891
- else {
892
- that.doFindFunc(req, resource, function (err, queryObj) {
893
- if (err) {
894
- callback(err);
895
- }
896
- else {
897
- var query = resource.model.find(queryObj);
898
- if (idArray.length > 0) {
899
- query = query.where('_id').in(idArray);
900
- }
901
- query = query.find(findParam).select(hiddenFields);
902
- if (limit) {
903
- query = query.limit(limit);
904
- }
905
- if (skip) {
906
- query = query.skip(skip);
684
+ for (let element in paths) {
685
+ if (paths.hasOwnProperty(element) && element !== '__v') {
686
+ // check for schemas
687
+ if (paths[element].schema) {
688
+ let subSchemaInfo = this.preprocess(null, paths[element].schema.paths);
689
+ outPath[element] = { schema: subSchemaInfo.paths };
690
+ if (paths[element].options.form) {
691
+ outPath[element].options = { form: extend(true, {}, paths[element].options.form) };
907
692
  }
908
- if (sortOrder) {
909
- query = query.sort(sortOrder);
693
+ // this provides support for entire nested schemas that wish to remain hidden
694
+ if (paths[element].options.secure) {
695
+ hiddenFields.push(element);
910
696
  }
911
- query.exec(callback);
697
+ // to support hiding individual properties of nested schema would require us
698
+ // to do something with subSchemaInfo.hide here
912
699
  }
913
- });
700
+ else {
701
+ // check for arrays
702
+ let realType = paths[element].caster ? paths[element].caster : paths[element];
703
+ if (!realType.instance) {
704
+ if (realType.options.type) {
705
+ let type = realType.options.type(), typeType = typeof type;
706
+ if (typeType === 'string') {
707
+ realType.instance = (!isNaN(Date.parse(type))) ? 'Date' : 'String';
708
+ }
709
+ else {
710
+ realType.instance = typeType;
711
+ }
712
+ }
713
+ }
714
+ outPath[element] = extend(true, {}, paths[element]);
715
+ if (paths[element].options.secure) {
716
+ hiddenFields.push(element);
717
+ }
718
+ if (paths[element].options.match) {
719
+ outPath[element].options.match = paths[element].options.match.source || paths[element].options.match;
720
+ }
721
+ let schemaListInfo = paths[element].options.list;
722
+ if (schemaListInfo) {
723
+ let listFieldInfo = { field: element };
724
+ if (typeof schemaListInfo === 'object' && Object.keys(schemaListInfo).length > 0) {
725
+ listFieldInfo.params = schemaListInfo;
726
+ }
727
+ listFields.push(listFieldInfo);
728
+ }
729
+ }
730
+ }
914
731
  }
915
- });
916
- };
917
- DataForm.prototype.collectionPost = function () {
918
- return _.bind(function (req, res, next) {
919
- this.processCollection(req);
920
- if (!req.resource) {
921
- next();
922
- return;
732
+ outPath = this.applySchemaSubset(outPath, formSchema);
733
+ let returnObj = { paths: outPath };
734
+ if (hiddenFields.length > 0) {
735
+ returnObj.hide = hiddenFields;
923
736
  }
924
- if (!req.body) {
925
- throw new Error('Nothing submitted.');
737
+ if (listFields.length > 0) {
738
+ returnObj.listFields = listFields;
926
739
  }
927
- var cleansedBody = this.cleanseRequest(req);
928
- req.doc = new req.resource.model(cleansedBody);
929
- this.saveAndRespond(req, res);
930
- }, this);
931
- };
932
- /**
933
- * Generate an object of fields to not expose
934
- **/
935
- DataForm.prototype.generateHiddenFields = function (resource, state) {
936
- var hiddenFields = {};
937
- if (resource.options['hide'] !== undefined) {
938
- resource.options.hide.forEach(function (dt) {
939
- hiddenFields[dt] = state;
940
- });
740
+ return returnObj;
941
741
  }
942
- return hiddenFields;
943
- };
944
- /** Sec issue
945
- * Cleanse incoming data to avoid overwrite and POST request forgery
946
- * (name may seem weird but it was in French, so it is some small improvement!)
947
- */
948
- DataForm.prototype.cleanseRequest = function (req) {
949
- var reqData = req.body, resource = req.resource;
950
- delete reqData.__v; // Don't mess with Mongoose internal field (https://github.com/LearnBoost/mongoose/issues/1933)
951
- if (typeof resource.options['hide'] === 'undefined') {
952
- return reqData;
742
+ ;
743
+ schema() {
744
+ return _.bind(function (req, res) {
745
+ if (!(req.resource = this.getResource(req.params.resourceName))) {
746
+ return res.status(404).end();
747
+ }
748
+ let formSchema = null;
749
+ if (req.params.formName) {
750
+ formSchema = req.resource.model.schema.statics['form'](req.params.formName, req);
751
+ }
752
+ let paths = this.preprocess(req.resource, req.resource.model.schema.paths, formSchema).paths;
753
+ res.send(paths);
754
+ }, this);
953
755
  }
954
- var hiddenFields = resource.options.hide;
955
- _.each(reqData, function (num, key) {
956
- _.each(hiddenFields, function (fi) {
957
- if (fi === key) {
958
- delete reqData[key];
756
+ ;
757
+ report() {
758
+ return _.bind(async function (req, res, next) {
759
+ if (!(req.resource = this.getResource(req.params.resourceName))) {
760
+ return next();
761
+ }
762
+ const self = this;
763
+ if (typeof req.query === 'undefined') {
764
+ req.query = {};
765
+ }
766
+ let reportSchema;
767
+ if (req.params.reportName) {
768
+ reportSchema = await req.resource.model.schema.statics['report'](req.params.reportName, req);
769
+ }
770
+ else if (req.query.r) {
771
+ switch (req.query.r[0]) {
772
+ case '[':
773
+ reportSchema = { pipeline: JSON.parse(req.query.r) };
774
+ break;
775
+ case '{':
776
+ reportSchema = JSON.parse(req.query.r);
777
+ break;
778
+ default:
779
+ return self.renderError(new Error('Invalid "r" parameter'), null, req, res);
780
+ }
781
+ }
782
+ else {
783
+ let fields = {};
784
+ for (let key in req.resource.model.schema.paths) {
785
+ if (req.resource.model.schema.paths.hasOwnProperty(key)) {
786
+ if (key !== '__v' && !req.resource.model.schema.paths[key].options.secure) {
787
+ if (key.indexOf('.') === -1) {
788
+ fields[key] = 1;
789
+ }
790
+ }
791
+ }
792
+ }
793
+ reportSchema = {
794
+ pipeline: [
795
+ { $project: fields }
796
+ ], drilldown: req.params.resourceName + '/|_id|/edit'
797
+ };
798
+ }
799
+ // Replace parameters in pipeline
800
+ let schemaCopy = {};
801
+ extend(schemaCopy, reportSchema);
802
+ schemaCopy.params = schemaCopy.params || [];
803
+ self.reportInternal(req, req.resource, schemaCopy, function (err, result) {
804
+ if (err) {
805
+ res.send({ success: false, error: err.message || err });
806
+ }
807
+ else {
808
+ res.send(result);
809
+ }
810
+ });
811
+ }, this);
812
+ }
813
+ ;
814
+ hackVariablesInPipeline(runPipeline) {
815
+ for (let pipelineSection = 0; pipelineSection < runPipeline.length; pipelineSection++) {
816
+ if (runPipeline[pipelineSection]['$match']) {
817
+ this.hackVariables(runPipeline[pipelineSection]['$match']);
959
818
  }
960
- });
961
- });
962
- return reqData;
963
- };
964
- DataForm.prototype.generateQueryForEntity = function (req, resource, id, cb) {
965
- var hiddenFields = this.generateHiddenFields(resource, false);
966
- hiddenFields.__v = 0;
967
- this.doFindFunc(req, resource, function (err, queryObj) {
968
- if (err) {
969
- cb(err);
970
819
  }
971
- else {
972
- var idSel = { _id: id };
973
- var crit = queryObj ? extend(queryObj, idSel) : idSel;
974
- cb(null, resource.model.findOne(crit).select(hiddenFields));
820
+ }
821
+ ;
822
+ hackVariables(obj) {
823
+ // Replace variables that cannot be serialised / deserialised. Bit of a hack, but needs must...
824
+ // Anything formatted 1800-01-01T00:00:00.000Z or 1800-01-01T00:00:00.000+0000 is converted to a Date
825
+ // Only handles the cases I need for now
826
+ // TODO: handle arrays etc
827
+ for (const prop in obj) {
828
+ if (obj.hasOwnProperty(prop)) {
829
+ if (typeof obj[prop] === 'string') {
830
+ const dateTest = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3})(Z|[+ -]\d{4})$/.exec(obj[prop]);
831
+ if (dateTest) {
832
+ obj[prop] = new Date(dateTest[1] + 'Z');
833
+ }
834
+ else {
835
+ const objectIdTest = /^([0-9a-fA-F]{24})$/.exec(obj[prop]);
836
+ if (objectIdTest) {
837
+ obj[prop] = new this.mongoose.Types.ObjectId(objectIdTest[1]);
838
+ }
839
+ }
840
+ }
841
+ else if (_.isObject(obj[prop])) {
842
+ this.hackVariables(obj[prop]);
843
+ }
844
+ }
975
845
  }
976
- });
977
- };
978
- /*
979
- * Entity request goes there first
980
- * It retrieves the resource
981
- */
982
- DataForm.prototype.processEntity = function (req, res, next) {
983
- if (!(req.resource = this.getResource(req.params.resourceName))) {
984
- next();
985
- return;
986
846
  }
987
- this.generateQueryForEntity(req, req.resource, req.params.id, function (err, query) {
988
- if (err) {
989
- return res.send({
990
- success: false,
991
- err: util.inspect(err)
992
- });
847
+ ;
848
+ sanitisePipeline(aggregationParam, hiddenFields, findFuncQry) {
849
+ let that = this;
850
+ let array = Array.isArray(aggregationParam) ? aggregationParam : [aggregationParam];
851
+ let retVal = [];
852
+ let doneHiddenFields = false;
853
+ if (findFuncQry) {
854
+ retVal.unshift({ $match: findFuncQry });
993
855
  }
994
- else {
995
- query.exec(function (err, doc) {
996
- if (err) {
997
- return res.send({
998
- success: false,
999
- err: util.inspect(err)
1000
- });
856
+ for (let pipelineSection = 0; pipelineSection < array.length; pipelineSection++) {
857
+ let stage = array[pipelineSection];
858
+ let keys = Object.keys(stage);
859
+ if (keys.length !== 1) {
860
+ throw new Error('Invalid pipeline instruction');
861
+ }
862
+ switch (keys[0]) {
863
+ case '$merge':
864
+ case '$out':
865
+ throw new Error('Cannot use potentially destructive pipeline stages');
866
+ case '$unionWith':
867
+ /*
868
+ Sanitise the pipeline we are doing a union with, removing hidden fields from that collection
869
+ */
870
+ const unionCollectionName = stage.$unionWith.coll;
871
+ const unionResource = that.getResourceFromCollection(unionCollectionName);
872
+ let unionHiddenLookupFields = {};
873
+ if (unionResource) {
874
+ if (unionResource.options?.hide?.length > 0) {
875
+ unionHiddenLookupFields = this.generateHiddenFields(unionResource, false);
876
+ }
877
+ }
878
+ stage.$unionWith.pipeline = that.sanitisePipeline(stage.$unionWith.pipeline, unionHiddenLookupFields, findFuncQry);
879
+ break;
880
+ case '$match':
881
+ this.hackVariables(array[pipelineSection]['$match']);
882
+ retVal.push(array[pipelineSection]);
883
+ if (!doneHiddenFields && Object.keys(hiddenFields) && Object.keys(hiddenFields).length > 0) {
884
+ // We can now project out the hidden fields (we wait for the $match to make sure we don't break
885
+ // a select that uses a hidden field
886
+ retVal.push({ $project: hiddenFields });
887
+ doneHiddenFields = true;
888
+ }
889
+ stage = null;
890
+ break;
891
+ case '$lookup':
892
+ // hide any hiddenfields in the lookup collection
893
+ const collectionName = stage.$lookup.from;
894
+ const lookupField = stage.$lookup.as;
895
+ if ((collectionName + lookupField).indexOf('$') !== -1) {
896
+ throw new Error('No support for lookups where the "from" or "as" is anything other than a simple string');
897
+ }
898
+ const resource = that.getResourceFromCollection(collectionName);
899
+ if (resource) {
900
+ if (resource.options?.hide?.length > 0) {
901
+ const hiddenLookupFields = this.generateHiddenFields(resource, false);
902
+ let hiddenFieldsObj = {};
903
+ Object.keys(hiddenLookupFields).forEach(hf => {
904
+ hiddenFieldsObj[`${lookupField}.${hf}`] = false;
905
+ });
906
+ retVal.push({ $project: hiddenFieldsObj });
907
+ }
908
+ }
909
+ break;
910
+ default:
911
+ // nothing
912
+ break;
913
+ }
914
+ if (stage) {
915
+ retVal.push(stage);
916
+ }
917
+ }
918
+ if (!doneHiddenFields && Object.keys(hiddenFields) && Object.keys(hiddenFields).length > 0) {
919
+ // If there was no $match we still need to hide the hidden fields
920
+ retVal.unshift({ $project: hiddenFields });
921
+ }
922
+ return retVal;
923
+ }
924
+ reportInternal(req, resource, schema, callback) {
925
+ let runPipelineStr;
926
+ let runPipelineObj;
927
+ let self = this;
928
+ if (typeof req.query === 'undefined') {
929
+ req.query = {};
930
+ }
931
+ self.doFindFunc(req, resource, function (err, queryObj) {
932
+ if (err) {
933
+ return 'There was a problem with the findFunc for model';
934
+ }
935
+ else {
936
+ runPipelineStr = JSON.stringify(schema.pipeline);
937
+ for (let param in req.query) {
938
+ if (req.query.hasOwnProperty(param)) {
939
+ if (req.query[param]) {
940
+ if (param !== 'r') { // we don't want to copy the whole report schema (again!)
941
+ if (schema.params[param] !== undefined) {
942
+ schema.params[param].value = req.query[param];
943
+ }
944
+ else {
945
+ callback(`No such parameter as ${param} - try one of ${Object.keys(schema.params).join()}`);
946
+ }
947
+ }
948
+ }
949
+ }
1001
950
  }
1002
- else if (doc == null) {
1003
- return res.send({
1004
- success: false,
1005
- err: 'Record not found'
951
+ // Replace parameters with the value
952
+ if (runPipelineStr) {
953
+ runPipelineStr = runPipelineStr.replace(/"\(.+?\)"/g, function (match) {
954
+ let sparam = schema.params[match.slice(2, -2)];
955
+ if (sparam !== undefined) {
956
+ if (sparam.type === 'number') {
957
+ return sparam.value;
958
+ }
959
+ else if (_.isObject(sparam.value)) {
960
+ return JSON.stringify(sparam.value);
961
+ }
962
+ else if (sparam.value[0] === '{') {
963
+ return sparam.value;
964
+ }
965
+ else {
966
+ return '"' + sparam.value + '"';
967
+ }
968
+ }
969
+ else {
970
+ callback(`No such parameter as ${match.slice(2, -2)} - try one of ${Object.keys(schema.params).join()}`);
971
+ }
1006
972
  });
1007
973
  }
1008
- req.doc = doc;
1009
- next();
974
+ runPipelineObj = JSON.parse(runPipelineStr);
975
+ let hiddenFields = self.generateHiddenFields(resource, false);
976
+ let toDo = {
977
+ runAggregation: function (cb) {
978
+ runPipelineObj = self.sanitisePipeline(runPipelineObj, hiddenFields, queryObj);
979
+ resource.model.aggregate(runPipelineObj, cb);
980
+ }
981
+ };
982
+ let translations = []; // array of form {ref:'lookupname',translations:[{value:xx, display:' '}]}
983
+ // if we need to do any column translations add the function to the tasks list
984
+ if (schema.columnTranslations) {
985
+ toDo.applyTranslations = ['runAggregation', function (results, cb) {
986
+ function doATranslate(column, theTranslation) {
987
+ results['runAggregation'].forEach(function (resultRow) {
988
+ let valToTranslate = resultRow[column.field];
989
+ valToTranslate = (valToTranslate ? valToTranslate.toString() : '');
990
+ let thisTranslation = _.find(theTranslation.translations, function (option) {
991
+ return valToTranslate === option.value.toString();
992
+ });
993
+ resultRow[column.field] = thisTranslation ? thisTranslation.display : ' * Missing columnTranslation * ';
994
+ });
995
+ }
996
+ schema.columnTranslations.forEach(function (columnTranslation) {
997
+ if (columnTranslation.translations) {
998
+ doATranslate(columnTranslation, columnTranslation);
999
+ }
1000
+ if (columnTranslation.ref) {
1001
+ let theTranslation = _.find(translations, function (translation) {
1002
+ return (translation.ref === columnTranslation.ref);
1003
+ });
1004
+ if (theTranslation) {
1005
+ doATranslate(columnTranslation, theTranslation);
1006
+ }
1007
+ else {
1008
+ cb('Invalid ref property of ' + columnTranslation.ref + ' in columnTranslations ' + columnTranslation.field);
1009
+ }
1010
+ }
1011
+ });
1012
+ cb(null, null);
1013
+ }];
1014
+ let callFuncs = false;
1015
+ for (let i = 0; i < schema.columnTranslations.length; i++) {
1016
+ let thisColumnTranslation = schema.columnTranslations[i];
1017
+ if (thisColumnTranslation.field) {
1018
+ // if any of the column translations are adhoc funcs, set up the tasks to perform them
1019
+ if (thisColumnTranslation.fn) {
1020
+ callFuncs = true;
1021
+ }
1022
+ // if this column translation is a "ref", set up the tasks to look up the values and populate the translations
1023
+ if (thisColumnTranslation.ref) {
1024
+ let lookup = self.getResource(thisColumnTranslation.ref);
1025
+ if (lookup) {
1026
+ if (!toDo[thisColumnTranslation.ref]) {
1027
+ let getFunc = function (ref) {
1028
+ let lookup = ref;
1029
+ return function (cb) {
1030
+ let translateObject = { ref: lookup.resourceName, translations: [] };
1031
+ translations.push(translateObject);
1032
+ lookup.model.find({}, {}, { lean: true }, function (err, findResults) {
1033
+ if (err) {
1034
+ cb(err);
1035
+ }
1036
+ else {
1037
+ // TODO - this ref func can probably be done away with now that list fields can have ref
1038
+ let j = 0;
1039
+ async.whilst(function (cbtest) {
1040
+ cbtest(null, j < findResults.length);
1041
+ }, function (cbres) {
1042
+ let theResult = findResults[j];
1043
+ translateObject.translations[j] = translateObject.translations[j] || {};
1044
+ let theTranslation = translateObject.translations[j];
1045
+ j++;
1046
+ self.getListFields(lookup, theResult, function (err, description) {
1047
+ if (err) {
1048
+ cbres(err);
1049
+ }
1050
+ else {
1051
+ theTranslation.value = theResult._id;
1052
+ theTranslation.display = description;
1053
+ cbres(null);
1054
+ }
1055
+ });
1056
+ }, cb);
1057
+ }
1058
+ });
1059
+ };
1060
+ };
1061
+ toDo[thisColumnTranslation.ref] = getFunc(lookup);
1062
+ toDo.applyTranslations.unshift(thisColumnTranslation.ref); // Make sure we populate lookup before doing translation
1063
+ }
1064
+ }
1065
+ else {
1066
+ return callback('Invalid ref property of ' + thisColumnTranslation.ref + ' in columnTranslations ' + thisColumnTranslation.field);
1067
+ }
1068
+ }
1069
+ if (!thisColumnTranslation.translations && !thisColumnTranslation.ref && !thisColumnTranslation.fn) {
1070
+ return callback('A column translation needs a ref, fn or a translations property - ' + thisColumnTranslation.field + ' has neither');
1071
+ }
1072
+ }
1073
+ else {
1074
+ return callback('A column translation needs a field property');
1075
+ }
1076
+ }
1077
+ if (callFuncs) {
1078
+ toDo['callFunctions'] = ['runAggregation', function (results, cb) {
1079
+ async.each(results.runAggregation, function (row, cb) {
1080
+ for (let i = 0; i < schema.columnTranslations.length; i++) {
1081
+ let thisColumnTranslation = schema.columnTranslations[i];
1082
+ if (thisColumnTranslation.fn) {
1083
+ thisColumnTranslation.fn(row, cb);
1084
+ }
1085
+ }
1086
+ }, function () {
1087
+ cb(null);
1088
+ });
1089
+ }];
1090
+ toDo.applyTranslations.unshift('callFunctions'); // Make sure we do function before translating its result
1091
+ }
1092
+ }
1093
+ async.auto(toDo, function (err, results) {
1094
+ if (err) {
1095
+ callback(err);
1096
+ }
1097
+ else {
1098
+ // TODO: Could loop through schema.params and just send back the values
1099
+ callback(null, {
1100
+ success: true,
1101
+ schema: schema,
1102
+ report: results.runAggregation,
1103
+ paramsUsed: schema.params
1104
+ });
1105
+ }
1106
+ });
1107
+ }
1108
+ });
1109
+ }
1110
+ ;
1111
+ saveAndRespond(req, res, hiddenFields) {
1112
+ function internalSave(doc) {
1113
+ doc.save(function (err, doc2) {
1114
+ if (err) {
1115
+ let err2 = { status: 'err' };
1116
+ if (!err.errors) {
1117
+ err2.message = err.message;
1118
+ }
1119
+ else {
1120
+ extend(err2, err);
1121
+ }
1122
+ if (debug) {
1123
+ console.log('Error saving record: ' + JSON.stringify(err2));
1124
+ }
1125
+ res.status(400).send(err2);
1126
+ }
1127
+ else {
1128
+ doc2 = doc2.toObject();
1129
+ for (const hiddenField in hiddenFields) {
1130
+ if (hiddenFields.hasOwnProperty(hiddenField) && hiddenFields[hiddenField]) {
1131
+ let parts = hiddenField.split('.');
1132
+ let lastPart = parts.length - 1;
1133
+ let target = doc2;
1134
+ for (let i = 0; i < lastPart; i++) {
1135
+ if (target.hasOwnProperty(parts[i])) {
1136
+ target = target[parts[i]];
1137
+ }
1138
+ }
1139
+ if (target.hasOwnProperty(parts[lastPart])) {
1140
+ delete target[parts[lastPart]];
1141
+ }
1142
+ }
1143
+ }
1144
+ res.send(doc2);
1145
+ }
1146
+ });
1147
+ }
1148
+ let doc = req.doc;
1149
+ if (typeof req.resource.options.onSave === 'function') {
1150
+ req.resource.options.onSave(doc, req, function (err) {
1151
+ if (err) {
1152
+ throw err;
1153
+ }
1154
+ internalSave(doc);
1010
1155
  });
1011
1156
  }
1012
- });
1013
- };
1014
- /**
1015
- * Gets a single entity
1016
- *
1017
- * @return {Function} The function to use as route
1018
- */
1019
- DataForm.prototype.entityGet = function () {
1020
- return _.bind(function (req, res, next) {
1021
- this.processEntity(req, res, function () {
1157
+ else {
1158
+ internalSave(doc);
1159
+ }
1160
+ }
1161
+ ;
1162
+ /**
1163
+ * All entities REST functions have to go through this first.
1164
+ */
1165
+ processCollection(req) {
1166
+ req.resource = this.getResource(req.params.resourceName);
1167
+ }
1168
+ ;
1169
+ /**
1170
+ * Renders a view with the list of docs, which may be modified by query parameters
1171
+ */
1172
+ collectionGet() {
1173
+ return _.bind(function (req, res, next) {
1174
+ this.processCollection(req);
1022
1175
  if (!req.resource) {
1023
1176
  return next();
1024
1177
  }
1025
- return res.send(req.doc);
1026
- });
1027
- }, this);
1028
- };
1029
- DataForm.prototype.replaceHiddenFields = function (record, data) {
1030
- var self = this;
1031
- if (record) {
1032
- record._replacingHiddenFields = true;
1033
- _.each(data, function (value, name) {
1034
- if (_.isObject(value)) {
1035
- self.replaceHiddenFields(record[name], value);
1178
+ if (typeof req.query === 'undefined') {
1179
+ req.query = {};
1180
+ }
1181
+ try {
1182
+ const aggregationParam = req.query.a ? JSON.parse(req.query.a) : null;
1183
+ const findParam = req.query.f ? JSON.parse(req.query.f) : {};
1184
+ const projectParam = req.query.p ? JSON.parse(req.query.p) : {};
1185
+ const limitParam = req.query.l ? JSON.parse(req.query.l) : 0;
1186
+ const skipParam = req.query.s ? JSON.parse(req.query.s) : 0;
1187
+ const orderParam = req.query.o ? JSON.parse(req.query.o) : req.resource.options.listOrder;
1188
+ // Dates in aggregation must be Dates
1189
+ if (aggregationParam) {
1190
+ this.hackVariablesInPipeline(aggregationParam);
1191
+ }
1192
+ const self = this;
1193
+ this.filteredFind(req.resource, req, aggregationParam, findParam, projectParam, orderParam, limitParam, skipParam, function (err, docs) {
1194
+ if (err) {
1195
+ return self.renderError(err, null, req, res);
1196
+ }
1197
+ else {
1198
+ res.send(docs);
1199
+ }
1200
+ });
1201
+ }
1202
+ catch (e) {
1203
+ res.status(400).send(e.message);
1204
+ }
1205
+ }, this);
1206
+ }
1207
+ ;
1208
+ generateProjection(hiddenFields, projectParam) {
1209
+ let type;
1210
+ function setSelectType(typeChar, checkChar) {
1211
+ if (type === checkChar) {
1212
+ throw new Error('Cannot mix include and exclude fields in select');
1036
1213
  }
1037
1214
  else {
1038
- record[name] = value;
1215
+ type = typeChar;
1216
+ }
1217
+ }
1218
+ let retVal = hiddenFields;
1219
+ if (projectParam) {
1220
+ let projection = Object.keys(projectParam);
1221
+ if (projection.length > 0) {
1222
+ projection.forEach(p => {
1223
+ if (projectParam[p] === 0) {
1224
+ setSelectType('E', 'I');
1225
+ }
1226
+ else if (projectParam[p] === 1) {
1227
+ setSelectType('I', 'E');
1228
+ }
1229
+ else {
1230
+ throw new Error('Invalid projection: ' + projectParam);
1231
+ }
1232
+ });
1233
+ if (type && type === 'E') {
1234
+ // We are excluding fields - can just merge with hiddenFields
1235
+ Object.assign(retVal, projectParam, hiddenFields);
1236
+ }
1237
+ else {
1238
+ // We are selecting fields - make sure none are hidden
1239
+ retVal = projectParam;
1240
+ for (let h in hiddenFields) {
1241
+ if (hiddenFields.hasOwnProperty(h)) {
1242
+ delete retVal[h];
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ return retVal;
1249
+ }
1250
+ ;
1251
+ doFindFunc(req, resource, cb) {
1252
+ if (resource.options.findFunc) {
1253
+ resource.options.findFunc(req, cb);
1254
+ }
1255
+ else {
1256
+ cb(null);
1257
+ }
1258
+ }
1259
+ ;
1260
+ filteredFind(resource, req, aggregationParam, findParam, projectParam, sortOrder, limit, skip, callback) {
1261
+ const that = this;
1262
+ let hiddenFields = this.generateHiddenFields(resource, false);
1263
+ let stashAggregationResults;
1264
+ function doAggregation(queryObj, cb) {
1265
+ if (aggregationParam) {
1266
+ aggregationParam = that.sanitisePipeline(aggregationParam, hiddenFields, queryObj);
1267
+ void resource.model.aggregate(aggregationParam, function (err, aggregationResults) {
1268
+ if (err) {
1269
+ throw err;
1270
+ }
1271
+ else {
1272
+ stashAggregationResults = aggregationResults;
1273
+ cb(_.map(aggregationResults, function (obj) {
1274
+ return obj._id;
1275
+ }));
1276
+ }
1277
+ });
1278
+ }
1279
+ else {
1280
+ cb([]);
1281
+ }
1282
+ }
1283
+ that.doFindFunc(req, resource, function (err, queryObj) {
1284
+ if (err) {
1285
+ callback(err);
1286
+ }
1287
+ else {
1288
+ doAggregation(queryObj, function (idArray) {
1289
+ if (aggregationParam && idArray.length === 0) {
1290
+ callback(null, []);
1291
+ }
1292
+ else {
1293
+ let query = resource.model.find(queryObj);
1294
+ if (idArray.length > 0) {
1295
+ query = query.where('_id').in(idArray);
1296
+ }
1297
+ if (findParam) {
1298
+ query = query.find(findParam);
1299
+ }
1300
+ query = query.select(that.generateProjection(hiddenFields, projectParam));
1301
+ if (limit) {
1302
+ query = query.limit(limit);
1303
+ }
1304
+ if (skip) {
1305
+ query = query.skip(skip);
1306
+ }
1307
+ if (sortOrder) {
1308
+ query = query.sort(sortOrder);
1309
+ }
1310
+ query.exec(function (err, docs) {
1311
+ if (!err && stashAggregationResults) {
1312
+ docs.forEach(obj => {
1313
+ // Add any fields from the aggregation results whose field name starts __ to the mongoose Document
1314
+ let aggObj = stashAggregationResults.find(a => a._id.toString() === obj._id.toString());
1315
+ Object.keys(aggObj).forEach(k => {
1316
+ if (k.slice(0, 2) === '__') {
1317
+ obj[k] = aggObj[k];
1318
+ }
1319
+ });
1320
+ });
1321
+ }
1322
+ callback(err, docs);
1323
+ });
1324
+ }
1325
+ });
1039
1326
  }
1040
1327
  });
1041
- delete record._replacingHiddenFields;
1042
1328
  }
1043
- };
1044
- DataForm.prototype.entityPut = function () {
1045
- return _.bind(function (req, res, next) {
1046
- var that = this;
1047
- this.processEntity(req, res, function () {
1329
+ ;
1330
+ collectionPost() {
1331
+ return _.bind(function (req, res, next) {
1332
+ this.processCollection(req);
1048
1333
  if (!req.resource) {
1049
1334
  next();
1050
1335
  return;
@@ -1052,70 +1337,491 @@ DataForm.prototype.entityPut = function () {
1052
1337
  if (!req.body) {
1053
1338
  throw new Error('Nothing submitted.');
1054
1339
  }
1055
- var cleansedBody = that.cleanseRequest(req);
1056
- // Merge
1057
- _.each(cleansedBody, function (value, name) {
1058
- req.doc[name] = (value === '') ? undefined : value;
1340
+ let cleansedBody = this.cleanseRequest(req);
1341
+ req.doc = new req.resource.model(cleansedBody);
1342
+ this.saveAndRespond(req, res);
1343
+ }, this);
1344
+ }
1345
+ ;
1346
+ /**
1347
+ * Generate an object of fields to not expose
1348
+ **/
1349
+ generateHiddenFields(resource, state) {
1350
+ let hiddenFields = {};
1351
+ if (resource.options['hide'] !== undefined) {
1352
+ resource.options.hide.forEach(function (dt) {
1353
+ hiddenFields[dt] = state;
1354
+ });
1355
+ }
1356
+ return hiddenFields;
1357
+ }
1358
+ ;
1359
+ /** Sec issue
1360
+ * Cleanse incoming data to avoid overwrite and POST request forgery
1361
+ * (name may seem weird but it was in French, so it is some small improvement!)
1362
+ */
1363
+ cleanseRequest(req) {
1364
+ let reqData = req.body, resource = req.resource;
1365
+ delete reqData.__v; // Don't mess with Mongoose internal field (https://github.com/LearnBoost/mongoose/issues/1933)
1366
+ if (typeof resource.options['hide'] === 'undefined') {
1367
+ return reqData;
1368
+ }
1369
+ let hiddenFields = resource.options.hide;
1370
+ _.each(reqData, function (num, key) {
1371
+ _.each(hiddenFields, function (fi) {
1372
+ if (fi === key) {
1373
+ delete reqData[key];
1374
+ }
1059
1375
  });
1060
- if (req.resource.options.hide !== undefined) {
1061
- var hiddenFields_1 = that.generateHiddenFields(req.resource, true);
1062
- hiddenFields_1._id = false;
1063
- req.resource.model.findById(req.doc._id, hiddenFields_1, { lean: true }, function (err, data) {
1064
- that.replaceHiddenFields(req.doc, data);
1065
- that.saveAndRespond(req, res, hiddenFields_1);
1376
+ });
1377
+ return reqData;
1378
+ }
1379
+ ;
1380
+ generateQueryForEntity(req, resource, id, cb) {
1381
+ let that = this;
1382
+ let hiddenFields = this.generateHiddenFields(resource, false);
1383
+ hiddenFields.__v = false;
1384
+ that.doFindFunc(req, resource, function (err, queryObj) {
1385
+ if (err) {
1386
+ cb(err);
1387
+ }
1388
+ else {
1389
+ const idSel = { _id: id };
1390
+ let crit;
1391
+ if (queryObj) {
1392
+ if (queryObj._id) {
1393
+ crit = { $and: [idSel, { _id: queryObj._id }] };
1394
+ delete queryObj._id;
1395
+ if (Object.keys(queryObj).length > 0) {
1396
+ crit = extend(crit, queryObj);
1397
+ }
1398
+ }
1399
+ else {
1400
+ crit = extend(queryObj, idSel);
1401
+ }
1402
+ }
1403
+ else {
1404
+ crit = idSel;
1405
+ }
1406
+ cb(null, resource.model.findOne(crit).select(that.generateProjection(hiddenFields, req.query?.p)));
1407
+ }
1408
+ });
1409
+ }
1410
+ ;
1411
+ /*
1412
+ * Entity request goes here first
1413
+ * It retrieves the resource
1414
+ */
1415
+ processEntity(req, res, next) {
1416
+ if (!(req.resource = this.getResource(req.params.resourceName))) {
1417
+ next();
1418
+ return;
1419
+ }
1420
+ this.generateQueryForEntity(req, req.resource, req.params.id, function (err, query) {
1421
+ if (err) {
1422
+ return res.status(500).send({
1423
+ success: false,
1424
+ err: util.inspect(err)
1066
1425
  });
1067
1426
  }
1068
1427
  else {
1069
- that.saveAndRespond(req, res);
1428
+ query.exec(function (err, doc) {
1429
+ if (err) {
1430
+ return res.status(400).send({
1431
+ success: false,
1432
+ err: util.inspect(err)
1433
+ });
1434
+ }
1435
+ else if (doc == null) {
1436
+ return res.status(404).send({
1437
+ success: false,
1438
+ err: 'Record not found'
1439
+ });
1440
+ }
1441
+ req.doc = doc;
1442
+ next();
1443
+ });
1070
1444
  }
1071
1445
  });
1072
- }, this);
1073
- };
1074
- DataForm.prototype.entityDelete = function () {
1075
- return _.bind(function (req, res, next) {
1076
- function internalRemove(doc) {
1077
- doc.remove(function (err) {
1078
- if (err) {
1079
- return res.send({ success: false });
1446
+ }
1447
+ ;
1448
+ /**
1449
+ * Gets a single entity
1450
+ *
1451
+ * @return {Function} The function to use as route
1452
+ */
1453
+ entityGet() {
1454
+ return _.bind(function (req, res, next) {
1455
+ this.processEntity(req, res, function () {
1456
+ if (!req.resource) {
1457
+ return next();
1458
+ }
1459
+ if (req.resource.options.onAccess) {
1460
+ req.resource.options.onAccess(req, function () {
1461
+ return res.status(200).send(req.doc);
1462
+ });
1463
+ }
1464
+ else {
1465
+ return res.status(200).send(req.doc);
1080
1466
  }
1081
- return res.send({ success: true });
1082
1467
  });
1468
+ }, this);
1469
+ }
1470
+ ;
1471
+ replaceHiddenFields(record, data) {
1472
+ const self = this;
1473
+ if (record) {
1474
+ record._replacingHiddenFields = true;
1475
+ _.each(data, function (value, name) {
1476
+ if (_.isObject(value) && !Array.isArray(value)) {
1477
+ self.replaceHiddenFields(record[name], value);
1478
+ }
1479
+ else if (!record[name]) {
1480
+ record[name] = value;
1481
+ }
1482
+ });
1483
+ delete record._replacingHiddenFields;
1083
1484
  }
1084
- this.processEntity(req, res, function () {
1085
- if (!req.resource) {
1086
- next();
1087
- return;
1485
+ }
1486
+ ;
1487
+ entityPut() {
1488
+ return _.bind(function (req, res, next) {
1489
+ const that = this;
1490
+ this.processEntity(req, res, function () {
1491
+ if (!req.resource) {
1492
+ next();
1493
+ return;
1494
+ }
1495
+ if (!req.body) {
1496
+ throw new Error('Nothing submitted.');
1497
+ }
1498
+ let cleansedBody = that.cleanseRequest(req);
1499
+ // Merge
1500
+ for (let prop in cleansedBody) {
1501
+ if (cleansedBody.hasOwnProperty(prop)) {
1502
+ req.doc.set(prop, cleansedBody[prop] === '' ? undefined : cleansedBody[prop]);
1503
+ }
1504
+ }
1505
+ if (req.resource.options.hide !== undefined) {
1506
+ let hiddenFields = that.generateHiddenFields(req.resource, true);
1507
+ hiddenFields._id = false;
1508
+ req.resource.model.findById(req.doc._id, hiddenFields, { lean: true }, function (err, data) {
1509
+ that.replaceHiddenFields(req.doc, data);
1510
+ that.saveAndRespond(req, res, hiddenFields);
1511
+ });
1512
+ }
1513
+ else {
1514
+ that.saveAndRespond(req, res);
1515
+ }
1516
+ });
1517
+ }, this);
1518
+ }
1519
+ ;
1520
+ entityDelete() {
1521
+ let that = this;
1522
+ return _.bind(async function (req, res, next) {
1523
+ function generateDependencyList(resource) {
1524
+ if (resource.options.dependents === undefined) {
1525
+ resource.options.dependents = that.resources.reduce(function (acc, r) {
1526
+ function searchPaths(schema, prefix) {
1527
+ var fldList = [];
1528
+ for (var fld in schema.paths) {
1529
+ if (schema.paths.hasOwnProperty(fld)) {
1530
+ var parts = fld.split('.');
1531
+ var schemaType = schema.tree;
1532
+ while (parts.length > 0) {
1533
+ schemaType = schemaType[parts.shift()];
1534
+ }
1535
+ if (schemaType.type) {
1536
+ if (schemaType.type.name === 'ObjectId' && schemaType.ref === resource.resourceName) {
1537
+ fldList.push(prefix + fld);
1538
+ }
1539
+ else if (_.isArray(schemaType.type)) {
1540
+ schemaType.type.forEach(function (t) {
1541
+ searchPaths(t, prefix + fld + '.');
1542
+ });
1543
+ }
1544
+ }
1545
+ }
1546
+ }
1547
+ if (fldList.length > 0) {
1548
+ acc.push({ resource: r, keys: fldList });
1549
+ }
1550
+ }
1551
+ searchPaths(r.model.schema, '');
1552
+ return acc;
1553
+ }, []);
1554
+ for (let pluginName in that.options.plugins) {
1555
+ let thisPlugin = that.options.plugins[pluginName];
1556
+ if (thisPlugin.dependencyChecks && thisPlugin.dependencyChecks[resource.resourceName]) {
1557
+ resource.options.dependents = resource.options.dependents.concat(thisPlugin.dependencyChecks[resource.resourceName]);
1558
+ }
1559
+ }
1560
+ }
1088
1561
  }
1089
- var doc = req.doc;
1090
- if (typeof req.resource.options.onRemove === 'function') {
1091
- req.resource.options.onRemove(doc, req, function (err) {
1092
- if (err) {
1093
- throw err;
1562
+ async function removeDoc(doc, resource) {
1563
+ switch (resource.options.handleRemove) {
1564
+ case 'allow':
1565
+ // old behaviour - no attempt to maintain data integrity
1566
+ return doc.remove();
1567
+ case 'cascade':
1568
+ generateDependencyList(resource);
1569
+ res.status(400).send('"cascade" option not yet supported');
1570
+ break;
1571
+ default:
1572
+ generateDependencyList(resource);
1573
+ let promises = [];
1574
+ resource.options.dependents.forEach(collection => {
1575
+ collection.keys.forEach(key => {
1576
+ promises.push({
1577
+ p: collection.resource.model.find({ [key]: doc._id }).limit(1).exec(),
1578
+ collection,
1579
+ key
1580
+ });
1581
+ });
1582
+ });
1583
+ return Promise.all(promises.map(p => p.p))
1584
+ .then((results) => {
1585
+ results.forEach((r, i) => {
1586
+ if (r.length > 0) {
1587
+ throw new ForeignKeyError(resource.resourceName, promises[i].collection.resource.resourceName, promises[i].key, r[0]._id);
1588
+ }
1589
+ });
1590
+ return doc.remove();
1591
+ });
1592
+ }
1593
+ }
1594
+ async function runDeletion(doc, resource) {
1595
+ return new Promise((resolve) => {
1596
+ if (resource.options.onRemove) {
1597
+ resource.options.onRemove(doc, req, async function (err) {
1598
+ if (err) {
1599
+ throw err;
1600
+ }
1601
+ resolve(removeDoc(doc, resource));
1602
+ });
1603
+ }
1604
+ else {
1605
+ resolve(removeDoc(doc, resource));
1094
1606
  }
1095
- internalRemove(doc);
1096
1607
  });
1097
1608
  }
1609
+ this.processEntity(req, res, async function () {
1610
+ if (!req.resource) {
1611
+ next();
1612
+ return;
1613
+ }
1614
+ let doc = req.doc;
1615
+ try {
1616
+ void await runDeletion(doc, req.resource);
1617
+ res.status(200).send();
1618
+ }
1619
+ catch (e) {
1620
+ if (e instanceof ForeignKeyError) {
1621
+ res.status(400).send(e.message);
1622
+ }
1623
+ else {
1624
+ res.status(500).send(e.message);
1625
+ }
1626
+ }
1627
+ });
1628
+ }, this);
1629
+ }
1630
+ ;
1631
+ entityList() {
1632
+ return _.bind(function (req, res, next) {
1633
+ const that = this;
1634
+ this.processEntity(req, res, function () {
1635
+ if (!req.resource) {
1636
+ return next();
1637
+ }
1638
+ const returnRawParam = req.query?.returnRaw ? !!JSON.parse(req.query.returnRaw) : false;
1639
+ if (returnRawParam) {
1640
+ const result = { _id: req.doc._id };
1641
+ for (const field of req.resource.options.listFields) {
1642
+ result[field.field] = req.doc[field.field];
1643
+ }
1644
+ return res.send(result);
1645
+ }
1646
+ else {
1647
+ that.getListFields(req.resource, req.doc, function (err, display) {
1648
+ if (err) {
1649
+ return res.status(500).send(err);
1650
+ }
1651
+ else {
1652
+ return res.send({ list: display });
1653
+ }
1654
+ });
1655
+ }
1656
+ });
1657
+ }, this);
1658
+ }
1659
+ ;
1660
+ // To disambiguate the contents of items - assumed to be the results of a single resource lookup or search -
1661
+ // pass the result of this function as the second argument to the disambiguate() function.
1662
+ // equalityProps should identify the property(s) of the items that must ALL be equal for two items to
1663
+ // be considered ambiguous.
1664
+ // disambiguationResourceName should identify the resource whose list field(s) should be used to generate
1665
+ // the disambiguation text for ambiguous results later.
1666
+ buildSingleResourceAmbiguousRecordStore(items, equalityProps, disambiguationResourceName) {
1667
+ const ambiguousResults = [];
1668
+ for (let i = 0; i < items.length - 1; i++) {
1669
+ for (let j = i + 1; j < items.length; j++) {
1670
+ if (!equalityProps.some((p) => items[i][p] !== items[j][p])) {
1671
+ if (!ambiguousResults.includes(items[i])) {
1672
+ ambiguousResults.push(items[i]);
1673
+ }
1674
+ if (!ambiguousResults.includes(items[j])) {
1675
+ ambiguousResults.push(items[j]);
1676
+ }
1677
+ }
1678
+ }
1679
+ }
1680
+ return { [disambiguationResourceName]: ambiguousResults };
1681
+ }
1682
+ // An alternative to buildSingleResourceAmbiguousRecordStore() for use when disambiguating the results of a
1683
+ // multi-resource lookup or search. In this case, all items need to include the name of the resource that
1684
+ // will be used (when necessary) to yield their disambiguation text later. The property of items that holds
1685
+ // that resource name should be identified by the disambiguationResourceNameProp parameter.
1686
+ // The scary-looking templating used here ensures that the items really do all have an (optional) string
1687
+ // property with the name identified by disambiguationResourceNameProp. For the avoidance of doubt, "prop"
1688
+ // here could be anything - "foo in f" would achieve the same result.
1689
+ buildMultiResourceAmbiguousRecordStore(items, equalityProps, disambiguationResourceNameProp) {
1690
+ const store = {};
1691
+ for (let i = 0; i < items.length - 1; i++) {
1692
+ for (let j = i + 1; j < items.length; j++) {
1693
+ if (items[i][disambiguationResourceNameProp] &&
1694
+ items[i][disambiguationResourceNameProp] === items[j][disambiguationResourceNameProp] &&
1695
+ !equalityProps.some((p) => items[i][p] !== items[j][p])) {
1696
+ if (!store[items[i][disambiguationResourceNameProp]]) {
1697
+ store[items[i][disambiguationResourceNameProp]] = [];
1698
+ }
1699
+ if (!store[items[i][disambiguationResourceNameProp]].includes(items[i])) {
1700
+ store[items[i][disambiguationResourceNameProp]].push(items[i]);
1701
+ }
1702
+ if (!store[items[i][disambiguationResourceNameProp]].includes(items[j])) {
1703
+ store[items[i][disambiguationResourceNameProp]].push(items[j]);
1704
+ }
1705
+ }
1706
+ }
1707
+ }
1708
+ return store;
1709
+ }
1710
+ // return just the id and list fields for all of the records from req.resource.
1711
+ // list fields are those whose schema entry has a value for the "list" attribute (except where this is { ref: true })
1712
+ // if the resource has no explicit list fields identified, the first string field will be returned. if the resource
1713
+ // doesn't have any string fields either, the first (non-id) field will be returned.
1714
+ // usually, we will respond with an array of ILookupItem objects, where the .text property of those objects is the concatenation
1715
+ // of all of the document's list fields (space-seperated).
1716
+ // to request the documents without this transformation applied, include "c=true" in the query string.
1717
+ // the query string can also be used to filter and order the response, by providing values for "f" (find), "l" (limit),
1718
+ // "s" (skip) and/or "o" (order).
1719
+ // results will be disambiguated if req.resource includes disambiguation parameters in its resource options.
1720
+ // where c=true, the disambiguation will be added as a suffix to the .text property of the returned ILookupItem objects.
1721
+ // otherwise, if the resource has just one list field, the disambiguation will be appended to the values of that field, and if
1722
+ // it has multiple list fields, it will be returned as an additional property of the returned (untransformed) objects
1723
+ internalEntityListAll(req, callback) {
1724
+ const projection = this.generateListFieldProjection(req.resource);
1725
+ const listFields = Object.keys(projection);
1726
+ const aggregationParam = req.query.a ? JSON.parse(req.query.a) : null;
1727
+ const findParam = req.query.f ? JSON.parse(req.query.f) : {};
1728
+ const limitParam = req.query.l ? JSON.parse(req.query.l) : 0;
1729
+ const skipParam = req.query.s ? JSON.parse(req.query.s) : 0;
1730
+ const orderParam = req.query.o ? JSON.parse(req.query.o) : req.resource.options.listOrder;
1731
+ const concatenateParam = req.query.c ? JSON.parse(req.query.c) : true;
1732
+ const resOpts = req.resource.options;
1733
+ let disambiguationField;
1734
+ let disambiguationResourceName;
1735
+ if (resOpts?.disambiguation) {
1736
+ disambiguationField = resOpts.disambiguation.field;
1737
+ if (disambiguationField) {
1738
+ projection[disambiguationField] = 1;
1739
+ disambiguationResourceName = resOpts.disambiguation.resource;
1740
+ }
1741
+ }
1742
+ const that = this;
1743
+ this.filteredFind(req.resource, req, aggregationParam, findParam, projection, orderParam, limitParam, skipParam, function (err, docs) {
1744
+ if (err) {
1745
+ return callback(err);
1746
+ }
1098
1747
  else {
1099
- internalRemove(doc);
1748
+ docs = docs.map((d) => d.toObject());
1749
+ if (concatenateParam) {
1750
+ const transformed = docs.map((doc) => {
1751
+ let text = "";
1752
+ for (const field of listFields) {
1753
+ if (doc[field]) {
1754
+ if (text !== "") {
1755
+ text += " ";
1756
+ }
1757
+ text += doc[field];
1758
+ }
1759
+ }
1760
+ const disambiguationId = disambiguationField ? doc[disambiguationField] : undefined;
1761
+ return { id: doc._id, text, disambiguationId };
1762
+ });
1763
+ if (disambiguationResourceName) {
1764
+ that.disambiguate(transformed, that.buildSingleResourceAmbiguousRecordStore(transformed, ["text"], disambiguationResourceName), "disambiguationId", (item, disambiguationText) => {
1765
+ item.text += ` (${disambiguationText})`;
1766
+ }, (err) => {
1767
+ callback(err, transformed);
1768
+ });
1769
+ }
1770
+ else {
1771
+ return callback(null, transformed);
1772
+ }
1773
+ }
1774
+ else {
1775
+ if (disambiguationResourceName) {
1776
+ that.disambiguate(docs, that.buildSingleResourceAmbiguousRecordStore(docs, listFields, disambiguationResourceName), disambiguationField, (item, disambiguationText) => {
1777
+ if (listFields.length === 1) {
1778
+ item[listFields[0]] += ` (${disambiguationText})`;
1779
+ }
1780
+ else {
1781
+ // store the text against hard-coded property name "disambiguation", rather than (say) using
1782
+ // item[disambiguationResourceName], because if disambiguationResourceName === disambiguationField,
1783
+ // that value would end up being deleted again when the this.disambiguate() call (which we have
1784
+ // been called from) does its final tidy-up and deletes [disambiguationField] from all of the items in docs
1785
+ item.disambiguation = disambiguationText;
1786
+ }
1787
+ }, (err) => {
1788
+ callback(err, docs);
1789
+ });
1790
+ }
1791
+ else {
1792
+ return callback(null, docs);
1793
+ }
1794
+ }
1100
1795
  }
1101
1796
  });
1102
- }, this);
1103
- };
1104
- DataForm.prototype.entityList = function () {
1105
- return _.bind(function (req, res, next) {
1106
- var that = this;
1107
- this.processEntity(req, res, function () {
1108
- if (!req.resource) {
1797
+ }
1798
+ ;
1799
+ entityListAll() {
1800
+ return _.bind(function (req, res, next) {
1801
+ if (!(req.resource = this.getResource(req.params.resourceName))) {
1109
1802
  return next();
1110
1803
  }
1111
- that.getListFields(req.resource, req.doc, function (err, display) {
1804
+ this.internalEntityListAll(req, function (err, resultsObject) {
1112
1805
  if (err) {
1113
- return res.status(500).send(err);
1806
+ res.status(400, err);
1114
1807
  }
1115
1808
  else {
1116
- return res.send({ list: display });
1809
+ res.send(resultsObject);
1117
1810
  }
1118
1811
  });
1119
- });
1120
- }, this);
1121
- };
1812
+ }, this);
1813
+ }
1814
+ ;
1815
+ extractTimestampFromMongoID(record) {
1816
+ let timestamp = record.toString().substring(0, 8);
1817
+ return new Date(parseInt(timestamp, 16) * 1000);
1818
+ }
1819
+ }
1820
+ exports.FormsAngular = FormsAngular;
1821
+ class ForeignKeyError extends global.Error {
1822
+ constructor(resourceName, foreignKeyOnResource, foreignItem, id) {
1823
+ super(`Cannot delete this ${resourceName}, as it is the ${foreignItem} on ${foreignKeyOnResource} ${id}`);
1824
+ this.name = "ForeignKeyError";
1825
+ this.stack = new global.Error('').stack;
1826
+ }
1827
+ }