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