forms-angular 0.12.0-beta.27 → 0.12.0-beta.271

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