forms-angular 0.12.0-beta.3 → 0.12.0-beta.300

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