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