jxp 2.14.6 → 2.15.0
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/libs/datamunging.js +97 -80
- package/libs/docs.js +66 -10
- package/libs/jxp.js +83 -65
- package/libs/schema_description.js +66 -19
- package/package.json +13 -13
- package/templates/model.pug +24 -5
package/libs/datamunging.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
var _ = require("underscore");
|
|
2
2
|
|
|
3
|
-
var input1 = {
|
|
3
|
+
var input1 = {
|
|
4
4
|
due_date: '2016-02-01T00:00:00Z',
|
|
5
5
|
from_document: '',
|
|
6
6
|
allow_online_payment: 'false',
|
|
@@ -41,45 +41,51 @@ var input1 = {
|
|
|
41
41
|
id: '93490358',
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
var test1 = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
44
|
+
var test1 = {
|
|
45
|
+
due_date: '2016-02-01T00:00:00Z',
|
|
46
|
+
from_document: '',
|
|
47
|
+
allow_online_payment: 'false',
|
|
48
|
+
paid: 'true',
|
|
49
|
+
locked: 'false',
|
|
50
|
+
customer_id: '2446859',
|
|
51
|
+
modified: '2016-02-02T15:09:06.983',
|
|
52
|
+
created: '2016-02-01T21:09:37.873',
|
|
53
|
+
date: '2016-02-01T00:00:00Z',
|
|
54
|
+
inclusive: 'false',
|
|
55
|
+
discount_percentage: '0',
|
|
56
|
+
tax_reference: '',
|
|
57
|
+
discount: '0',
|
|
58
|
+
amount_due: '0',
|
|
59
|
+
printed: 'false',
|
|
60
|
+
editable: 'true',
|
|
61
|
+
has_attachments: 'false',
|
|
62
|
+
has_notes: 'false',
|
|
63
|
+
has_anticipated_date: 'false',
|
|
64
|
+
lines:
|
|
65
|
+
[{
|
|
66
|
+
SelectionId: '6824935',
|
|
67
|
+
TaxTypeId: '965302',
|
|
68
|
+
ID: '74465675',
|
|
69
|
+
LineType: '0',
|
|
70
|
+
Quantity: '1',
|
|
71
|
+
Unit: '',
|
|
72
|
+
TaxPercentage: '0.14',
|
|
73
|
+
DiscountPercentage: '0',
|
|
74
|
+
Discount: '0'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
ID: '74465676',
|
|
78
|
+
LineType: '0',
|
|
79
|
+
Quantity: '4',
|
|
80
|
+
Unit: '',
|
|
81
|
+
TaxPercentage: '0.14',
|
|
82
|
+
DiscountPercentage: '0',
|
|
83
|
+
Discount: '0',
|
|
84
|
+
Comments: ''
|
|
85
|
+
}],
|
|
86
|
+
location: '547d506b184d110c0601314c',
|
|
87
|
+
id: '93490358'
|
|
88
|
+
}
|
|
83
89
|
|
|
84
90
|
input2 = {
|
|
85
91
|
'test[0][Yo]': "0Yo",
|
|
@@ -161,39 +167,45 @@ var allMatch = /^[a-zA-Z\d_\-]+|\[[a-zA-Z\d_\-][a-zA-Z_\-\d]*\]|\[\d+\]/g;
|
|
|
161
167
|
|
|
162
168
|
// Based conceptually on the _.extend() function in underscore.js ( see http://documentcloud.github.com/underscore/#extend for more details )
|
|
163
169
|
|
|
164
|
-
function deepExtend(obj) {
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
function deepExtend(obj, depth = 0) {
|
|
171
|
+
const MAX_DEPTH = 20; // Prevent infinite recursion
|
|
172
|
+
if (depth > MAX_DEPTH) {
|
|
173
|
+
console.warn('Maximum depth exceeded in deepExtend');
|
|
174
|
+
return obj;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var parentRE = /#{\s*?_\s*?}/,
|
|
178
|
+
slice = Array.prototype.slice;
|
|
167
179
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
180
|
+
_.each(slice.call(arguments, 1), function (source) {
|
|
181
|
+
for (var prop in source) {
|
|
182
|
+
if (_.isUndefined(obj[prop]) || _.isFunction(obj[prop]) || _.isNull(source[prop]) || _.isDate(source[prop])) {
|
|
183
|
+
obj[prop] = source[prop];
|
|
184
|
+
}
|
|
185
|
+
else if (_.isString(source[prop]) && parentRE.test(source[prop])) {
|
|
186
|
+
if (_.isString(obj[prop])) {
|
|
187
|
+
obj[prop] = source[prop].replace(parentRE, obj[prop]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (_.isArray(obj[prop]) || _.isArray(source[prop])) {
|
|
191
|
+
if (!_.isArray(obj[prop]) || !_.isArray(source[prop])) {
|
|
192
|
+
throw new Error('Trying to combine an array with a non-array (' + prop + ')');
|
|
193
|
+
} else {
|
|
194
|
+
obj[prop] = _.reject(_.deepExtend(_.clone(obj[prop]), source[prop], depth + 1), function (item) { return _.isNull(item); });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (_.isObject(obj[prop]) || _.isObject(source[prop])) {
|
|
198
|
+
if (!_.isObject(obj[prop]) || !_.isObject(source[prop])) {
|
|
199
|
+
throw new Error('Trying to combine an object with a non-object (' + prop + ')');
|
|
200
|
+
} else {
|
|
201
|
+
obj[prop] = _.deepExtend(_.clone(obj[prop]), source[prop], depth + 1);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
obj[prop] = source[prop];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return obj;
|
|
197
209
|
};
|
|
198
210
|
|
|
199
211
|
_.mixin({ 'deepExtend': deepExtend });
|
|
@@ -259,7 +271,13 @@ function isArray(a) {
|
|
|
259
271
|
return Array.isArray(a);
|
|
260
272
|
}
|
|
261
273
|
|
|
262
|
-
var assignPropVal = function(parts, result, val) {
|
|
274
|
+
var assignPropVal = function (parts, result, val, depth = 0) {
|
|
275
|
+
const MAX_DEPTH = 20; // Prevent infinite recursion
|
|
276
|
+
if (depth > MAX_DEPTH) {
|
|
277
|
+
console.warn('Maximum depth exceeded in assignPropVal');
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
263
281
|
var part = parts.shift().replace(/\[|\]/g, "");
|
|
264
282
|
if (parts.length) {
|
|
265
283
|
if (isNumeric(parts[0].replace(/\[|\]/g, ""))) {
|
|
@@ -267,14 +285,14 @@ var assignPropVal = function(parts, result, val) {
|
|
|
267
285
|
} else {
|
|
268
286
|
result[part] = {};
|
|
269
287
|
}
|
|
270
|
-
assignPropVal(parts, result[part], val);
|
|
288
|
+
assignPropVal(parts, result[part], val, depth + 1);
|
|
271
289
|
} else {
|
|
272
290
|
result[part] = val;
|
|
273
291
|
}
|
|
274
292
|
return result;
|
|
275
293
|
}
|
|
276
294
|
|
|
277
|
-
var deserialize = function(input) {
|
|
295
|
+
var deserialize = function (input) {
|
|
278
296
|
// console.log("INPUT", input);
|
|
279
297
|
var result = {};
|
|
280
298
|
var newobj = {};
|
|
@@ -283,20 +301,19 @@ var deserialize = function(input) {
|
|
|
283
301
|
// console.log(prop, parts);
|
|
284
302
|
var val = input[prop];
|
|
285
303
|
if (parts) {
|
|
286
|
-
var tmp = assignPropVal(parts, result, val);
|
|
287
|
-
newobj = _.deepExtend(newobj, tmp);
|
|
304
|
+
var tmp = assignPropVal(parts, result, val, 0);
|
|
305
|
+
newobj = _.deepExtend(newobj, tmp, 0);
|
|
288
306
|
} else {
|
|
289
307
|
// console.log("NOT ARRAY", parts);
|
|
290
308
|
newobj[prop] = input[prop];
|
|
291
309
|
}
|
|
292
|
-
|
|
293
310
|
}
|
|
294
311
|
// input = newobj;
|
|
295
312
|
// console.log("Munge!", newobj);
|
|
296
313
|
return newobj;
|
|
297
314
|
}
|
|
298
315
|
|
|
299
|
-
var runTest = function(name, input, test) {
|
|
316
|
+
var runTest = function (name, input, test) {
|
|
300
317
|
var result = deserialize(input);
|
|
301
318
|
if (_.isEqual(result, test)) {
|
|
302
319
|
console.log("PASSED " + name);
|
|
@@ -306,7 +323,7 @@ var runTest = function(name, input, test) {
|
|
|
306
323
|
}
|
|
307
324
|
}
|
|
308
325
|
|
|
309
|
-
var test = function() {
|
|
326
|
+
var test = function () {
|
|
310
327
|
runTest("Test 1", input1, test1);
|
|
311
328
|
runTest("Test 2", input2, test2);
|
|
312
329
|
runTest("Test 3", input3, test3);
|
package/libs/docs.js
CHANGED
|
@@ -8,6 +8,50 @@ const readFile = util.promisify(fs.readFile);
|
|
|
8
8
|
const schema_description = require("./schema_description");
|
|
9
9
|
const errors = require("restify-errors");
|
|
10
10
|
|
|
11
|
+
// Helper function to safely serialize schema fields
|
|
12
|
+
const serializeField = (field) => {
|
|
13
|
+
const safeField = {
|
|
14
|
+
path: field.path,
|
|
15
|
+
instance: field.instance,
|
|
16
|
+
options: { ...field.options },
|
|
17
|
+
validators: field.validators?.map(v => ({
|
|
18
|
+
type: v.type?.name,
|
|
19
|
+
message: v.message
|
|
20
|
+
})),
|
|
21
|
+
isRequired: field.isRequired
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Handle default value
|
|
25
|
+
if (typeof field.defaultValue === 'function') {
|
|
26
|
+
safeField.defaultValue = '[Function]';
|
|
27
|
+
} else if (field.defaultValue === undefined && field.options.default !== undefined) {
|
|
28
|
+
if (typeof field.options.default === 'function') {
|
|
29
|
+
safeField.defaultValue = '[Function]';
|
|
30
|
+
} else {
|
|
31
|
+
safeField.defaultValue = field.options.default;
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
safeField.defaultValue = field.defaultValue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle special cases for different field types
|
|
38
|
+
if (field.instance === 'Array' && field.caster) {
|
|
39
|
+
safeField.arrayType = field.caster.instance;
|
|
40
|
+
// Handle array defaults specially
|
|
41
|
+
if (Array.isArray(safeField.defaultValue)) {
|
|
42
|
+
safeField.defaultValue = JSON.stringify(safeField.defaultValue);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (field.instance === 'Embedded' || field.instance === 'DocumentArray') {
|
|
46
|
+
safeField.schema = Object.keys(field.schema.paths).map(p => ({
|
|
47
|
+
path: p,
|
|
48
|
+
type: field.schema.paths[p].instance
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return safeField;
|
|
53
|
+
};
|
|
54
|
+
|
|
11
55
|
class Docs {
|
|
12
56
|
constructor(opts) {
|
|
13
57
|
this.config = Object.assign({}, opts.config);
|
|
@@ -16,7 +60,7 @@ class Docs {
|
|
|
16
60
|
this.package = require(path.join(process.cwd(), "package.json"));
|
|
17
61
|
}
|
|
18
62
|
|
|
19
|
-
renderTemplate(res, template_file, data={}) {
|
|
63
|
+
renderTemplate(res, template_file, data = {}) {
|
|
20
64
|
try {
|
|
21
65
|
const template = pug.compileFile(path.join(__dirname, `../templates/${template_file}.pug`));
|
|
22
66
|
data.title = data.title || `${this.package.name} API Documentation`;
|
|
@@ -28,10 +72,10 @@ class Docs {
|
|
|
28
72
|
res.writeHead(200, {
|
|
29
73
|
'Content-Length': Buffer.byteLength(body),
|
|
30
74
|
'Content-Type': 'text/html'
|
|
31
|
-
|
|
75
|
+
});
|
|
32
76
|
res.write(body);
|
|
33
77
|
res.end();
|
|
34
|
-
} catch(err) {
|
|
78
|
+
} catch (err) {
|
|
35
79
|
console.error(err);
|
|
36
80
|
return new errors.InternalServerError(err.toString());
|
|
37
81
|
}
|
|
@@ -64,7 +108,7 @@ class Docs {
|
|
|
64
108
|
try {
|
|
65
109
|
res.send(this.models);
|
|
66
110
|
next();
|
|
67
|
-
} catch(err) {
|
|
111
|
+
} catch (err) {
|
|
68
112
|
return new errors.InternalServerError(err.toString());
|
|
69
113
|
}
|
|
70
114
|
}
|
|
@@ -73,7 +117,7 @@ class Docs {
|
|
|
73
117
|
try {
|
|
74
118
|
this.renderTemplate(res, "index", {});
|
|
75
119
|
next();
|
|
76
|
-
} catch(err) {
|
|
120
|
+
} catch (err) {
|
|
77
121
|
return new errors.InternalServerError(err.toString());
|
|
78
122
|
}
|
|
79
123
|
}
|
|
@@ -83,7 +127,7 @@ class Docs {
|
|
|
83
127
|
const body = await readFile(path.join(__dirname, `../docs`, req.params.md_doc));
|
|
84
128
|
const md_contents = md.render(body.toString()).body;
|
|
85
129
|
this.renderTemplate(res, "md", { md_contents });
|
|
86
|
-
} catch(err) {
|
|
130
|
+
} catch (err) {
|
|
87
131
|
return new errors.InternalServerError(err.toString());
|
|
88
132
|
}
|
|
89
133
|
}
|
|
@@ -91,13 +135,25 @@ class Docs {
|
|
|
91
135
|
model(req, res, next) {
|
|
92
136
|
try {
|
|
93
137
|
const model = this.models[req.params.modelname];
|
|
94
|
-
|
|
95
|
-
const fields = Object.keys(model.schema.paths);
|
|
138
|
+
const fields = Object.keys(model.schema.paths);
|
|
96
139
|
fields.sort();
|
|
97
140
|
const perms = model.schema.opts.perms;
|
|
98
|
-
|
|
141
|
+
|
|
142
|
+
// Prepare safe field data for template
|
|
143
|
+
const safeFields = {};
|
|
144
|
+
fields.forEach(fieldName => {
|
|
145
|
+
safeFields[fieldName] = serializeField(model.schema.paths[fieldName]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this.renderTemplate(res, "model", {
|
|
149
|
+
model,
|
|
150
|
+
fields,
|
|
151
|
+
perms,
|
|
152
|
+
safeFields
|
|
153
|
+
});
|
|
99
154
|
next();
|
|
100
|
-
} catch(err) {
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(err);
|
|
101
157
|
return new errors.InternalServerError(err.toString());
|
|
102
158
|
}
|
|
103
159
|
}
|
package/libs/jxp.js
CHANGED
|
@@ -72,7 +72,7 @@ const outputCSV = (req, res, next) => {
|
|
|
72
72
|
try {
|
|
73
73
|
const data = res.result.data.map(row => row._doc);
|
|
74
74
|
if (!data.length) {
|
|
75
|
-
throw("")
|
|
75
|
+
throw ("")
|
|
76
76
|
}
|
|
77
77
|
res.writeHead(200, {
|
|
78
78
|
'Content-Type': 'text/csv',
|
|
@@ -91,7 +91,7 @@ const outputCSV = (req, res, next) => {
|
|
|
91
91
|
const actionGet = async (req, res) => {
|
|
92
92
|
const opname = `get ${req.modelname} ${ops++}`;
|
|
93
93
|
console.time(opname);
|
|
94
|
-
const parseSearch = function(search) {
|
|
94
|
+
const parseSearch = function (search) {
|
|
95
95
|
let result = {};
|
|
96
96
|
for (let i in search) {
|
|
97
97
|
result[i] = new RegExp(search[i], "i");
|
|
@@ -120,9 +120,9 @@ const actionGet = async (req, res) => {
|
|
|
120
120
|
}
|
|
121
121
|
if (req.query.search) {
|
|
122
122
|
// console.log({ search: req.query.search });
|
|
123
|
-
q = req.Model.find({ $text: { $search: req.query.search }}, { score
|
|
124
|
-
countquery = Object.assign({ $text: { $search: req.query.search }}, countquery);
|
|
125
|
-
qcount = req.Model.find({ $text: { $search: req.query.search }});
|
|
123
|
+
q = req.Model.find({ $text: { $search: req.query.search } }, { score: { $meta: "textScore" } }).sort({ score: { $meta: "textScore" } });
|
|
124
|
+
countquery = Object.assign({ $text: { $search: req.query.search } }, countquery);
|
|
125
|
+
qcount = req.Model.find({ $text: { $search: req.query.search } });
|
|
126
126
|
}
|
|
127
127
|
if (res.user) {
|
|
128
128
|
q.options = ({ user: res.user });
|
|
@@ -164,7 +164,7 @@ const actionGet = async (req, res) => {
|
|
|
164
164
|
} else {
|
|
165
165
|
q.populate(req.query.populate);
|
|
166
166
|
}
|
|
167
|
-
result.populate = req.query.populate;
|
|
167
|
+
result.populate = req.query.populate;
|
|
168
168
|
}
|
|
169
169
|
if (req.query.autopopulate) {
|
|
170
170
|
for (let key in req.Model.schema.paths) {
|
|
@@ -189,7 +189,7 @@ const actionGet = async (req, res) => {
|
|
|
189
189
|
result.data = await q.exec();
|
|
190
190
|
res.result = result;
|
|
191
191
|
if (debug) console.timeEnd(opname);
|
|
192
|
-
} catch(err) {
|
|
192
|
+
} catch (err) {
|
|
193
193
|
console.error(new Date(), err);
|
|
194
194
|
if (debug) console.timeEnd(opname);
|
|
195
195
|
if (err.code) throw err;
|
|
@@ -204,7 +204,7 @@ const actionGetOne = async (req, res) => {
|
|
|
204
204
|
const data = await getOne(req.Model, req.params.item_id, req.query, { user: res.user });
|
|
205
205
|
res.result = { data };
|
|
206
206
|
if (debug) console.timeEnd(opname);
|
|
207
|
-
} catch(err) {
|
|
207
|
+
} catch (err) {
|
|
208
208
|
console.error(new Date(), err);
|
|
209
209
|
if (debug) console.timeEnd(opname);
|
|
210
210
|
if (err.code) throw err;
|
|
@@ -262,7 +262,7 @@ const actionPut = async (req, res) => {
|
|
|
262
262
|
let silence = req.params._silence;
|
|
263
263
|
if (req.body && req.body._silence) silence = true;
|
|
264
264
|
if (!silence) {
|
|
265
|
-
req.config.callbacks.put.call(null, req.modelname, item, res.user
|
|
265
|
+
req.config.callbacks.put.call(null, req.modelname, item, res.user);
|
|
266
266
|
ws.putHook.call(null, req.modelname, item, res.user);
|
|
267
267
|
}
|
|
268
268
|
res.json({
|
|
@@ -283,12 +283,12 @@ const actionUpdate = async (req, res) => {
|
|
|
283
283
|
const opname = `update ${req.modelname}/${req.params.item_id} ${ops++}`;
|
|
284
284
|
console.time(opname);
|
|
285
285
|
try {
|
|
286
|
-
let body_data =
|
|
286
|
+
let body_data = datamunging.deserialize(req.body);
|
|
287
287
|
const data = await req.Model.update({ _id: req.params.item_id }, body_data);
|
|
288
288
|
let silence = req.params._silence;
|
|
289
289
|
if (req.body && req.body._silence) silence = true;
|
|
290
290
|
if (!silence) {
|
|
291
|
-
req.config.callbacks.put.call(null, req.modelname, data, res.user
|
|
291
|
+
req.config.callbacks.put.call(null, req.modelname, data, res.user);
|
|
292
292
|
ws.putHook.call(null, req.modelname, data, res.user);
|
|
293
293
|
}
|
|
294
294
|
res.json({
|
|
@@ -370,10 +370,10 @@ const actionDelete = async (req, res) => {
|
|
|
370
370
|
}
|
|
371
371
|
res.json({
|
|
372
372
|
status: "ok",
|
|
373
|
-
message: `${req.modelname}/${
|
|
373
|
+
message: `${req.modelname}/${req.params.item_id} deleted`
|
|
374
374
|
});
|
|
375
375
|
if (debug) console.timeEnd(opname);
|
|
376
|
-
} catch(err) {
|
|
376
|
+
} catch (err) {
|
|
377
377
|
console.error(new Date(), err);
|
|
378
378
|
if (debug) console.timeEnd(opname);
|
|
379
379
|
if (err.code) throw err;
|
|
@@ -384,7 +384,7 @@ const actionDelete = async (req, res) => {
|
|
|
384
384
|
const actionCount = async (req, res) => {
|
|
385
385
|
const opname = `count ${req.modelname} ${ops++}`;
|
|
386
386
|
console.time(opname);
|
|
387
|
-
const parseSearch = function(search) {
|
|
387
|
+
const parseSearch = function (search) {
|
|
388
388
|
let result = {};
|
|
389
389
|
for (let i in search) {
|
|
390
390
|
result[i] = new RegExp(search[i], "i");
|
|
@@ -409,7 +409,7 @@ const actionCount = async (req, res) => {
|
|
|
409
409
|
const count = await req.Model.countDocuments(filters).exec();
|
|
410
410
|
res.result = { count };
|
|
411
411
|
if (debug) console.timeEnd(opname);
|
|
412
|
-
} catch(err) {
|
|
412
|
+
} catch (err) {
|
|
413
413
|
console.error(new Date(), err);
|
|
414
414
|
if (debug) console.timeEnd(opname);
|
|
415
415
|
if (err.code) throw err;
|
|
@@ -424,7 +424,7 @@ const actionCall = async (req, res) => {
|
|
|
424
424
|
try {
|
|
425
425
|
const result = await req.Model[req.params.method_name](req.body);
|
|
426
426
|
res.json(result);
|
|
427
|
-
} catch(err) {
|
|
427
|
+
} catch (err) {
|
|
428
428
|
console.error(new Date(), err);
|
|
429
429
|
if (err.code) throw err;
|
|
430
430
|
throw new errors.InternalServerError(err.toString());
|
|
@@ -440,7 +440,7 @@ const actionCallItem = async (req, res) => {
|
|
|
440
440
|
req.params.__user = res.user || null;
|
|
441
441
|
const result = await req.Model[req.params.method_name](item);
|
|
442
442
|
res.json(result);
|
|
443
|
-
} catch(err) {
|
|
443
|
+
} catch (err) {
|
|
444
444
|
console.trace(err);
|
|
445
445
|
if (err.code) throw err;
|
|
446
446
|
throw new errors.InternalServerError(err.toString());
|
|
@@ -493,7 +493,7 @@ const actionQuery = async (req, res) => {
|
|
|
493
493
|
} else {
|
|
494
494
|
q.populate(req.query.populate);
|
|
495
495
|
}
|
|
496
|
-
result.populate = req.query.populate;
|
|
496
|
+
result.populate = req.query.populate;
|
|
497
497
|
}
|
|
498
498
|
if (req.query.autopopulate) {
|
|
499
499
|
for (let key in req.Model.schema.paths) {
|
|
@@ -516,7 +516,7 @@ const actionQuery = async (req, res) => {
|
|
|
516
516
|
res.result = result;
|
|
517
517
|
if (debug) console.timeEnd(opname);
|
|
518
518
|
res.json(result);
|
|
519
|
-
} catch(err) {
|
|
519
|
+
} catch (err) {
|
|
520
520
|
console.error(new Date(), err);
|
|
521
521
|
if (debug) console.timeEnd(opname);
|
|
522
522
|
if (err.code) throw err;
|
|
@@ -611,7 +611,7 @@ const actionBulkWrite = async (req, res) => {
|
|
|
611
611
|
const getOne = async (Model, item_id, params, options) => {
|
|
612
612
|
const query = Model.findById(item_id, {}, options);
|
|
613
613
|
if (params.populate) {
|
|
614
|
-
if ((typeof params.populate === "object")
|
|
614
|
+
if ((typeof params.populate === "object") && !Array.isArray(params.populate)) {
|
|
615
615
|
for (let i in params.populate) {
|
|
616
616
|
query.populate(i, params.populate[i].replace(/,/g, " "));
|
|
617
617
|
}
|
|
@@ -641,56 +641,71 @@ const getOne = async (Model, item_id, params, options) => {
|
|
|
641
641
|
//Don't ever return passwords
|
|
642
642
|
delete item.password;
|
|
643
643
|
return item;
|
|
644
|
-
} catch(err) {
|
|
644
|
+
} catch (err) {
|
|
645
645
|
console.error(err);
|
|
646
646
|
if (err.code) throw err;
|
|
647
647
|
throw new errors.InternalServerError(err.toString());
|
|
648
648
|
}
|
|
649
649
|
};
|
|
650
650
|
|
|
651
|
-
const parseFilter = (filter) => {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
651
|
+
const parseFilter = (filter, depth = 0) => {
|
|
652
|
+
const MAX_DEPTH = 10; // Prevent infinite recursion
|
|
653
|
+
|
|
654
|
+
if (!filter) return {};
|
|
655
|
+
if (depth > MAX_DEPTH) {
|
|
656
|
+
console.warn('Maximum filter depth exceeded');
|
|
657
|
+
return filter;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (typeof filter !== "object" || filter === null) return filter;
|
|
661
|
+
|
|
662
|
+
Object.keys(filter).forEach(function (key) {
|
|
663
|
+
var val = filter[key];
|
|
664
|
+
if (filter[key] === "false") filter[key] = false;
|
|
665
|
+
if (filter[key] === "true") filter[key] = true;
|
|
666
|
+
if (val && val.indexOf) {
|
|
667
|
+
if (val.indexOf(":") !== -1) {
|
|
668
|
+
let tmp = val.split(":");
|
|
669
|
+
filter[key] = {};
|
|
670
|
+
let tmpkey = tmp.shift();
|
|
671
|
+
let tmpval = tmp.join(":");
|
|
672
|
+
if ((tmpval[0] === "[") && (tmpval[tmpval.length - 1] === "]")) {
|
|
673
|
+
let arr = tmpval.slice(1, tmpval.length - 1).split(",");
|
|
674
|
+
tmpval = arr;
|
|
675
|
+
}
|
|
676
|
+
filter[key][tmpkey] = tmpval;
|
|
677
|
+
if (tmpkey === "$regex" && tmpval[0] === "/") {
|
|
678
|
+
let match = tmpval.match(new RegExp('^/(.*?)/([gimy]*)$'));
|
|
679
|
+
if (match) {
|
|
672
680
|
let regex = new RegExp(match[1], match[2]);
|
|
673
681
|
filter[key][tmpkey] = regex;
|
|
674
682
|
}
|
|
675
683
|
}
|
|
676
|
-
|
|
677
|
-
|
|
684
|
+
}
|
|
685
|
+
if (typeof val == "object") {
|
|
686
|
+
let result = parseFilter(val, depth + 1);
|
|
687
|
+
if (result && typeof result === 'object') {
|
|
678
688
|
filter[key] = {};
|
|
679
|
-
|
|
680
|
-
filter[key][
|
|
681
|
-
|
|
682
|
-
}
|
|
689
|
+
Object.keys(result).forEach(resultKey => {
|
|
690
|
+
filter[key][resultKey] = result[resultKey];
|
|
691
|
+
});
|
|
683
692
|
}
|
|
684
693
|
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
687
696
|
return filter;
|
|
688
697
|
}
|
|
689
698
|
|
|
690
699
|
const _deSerialize = (data) => {
|
|
691
700
|
function assign(obj, keyPath, value) {
|
|
692
|
-
//
|
|
701
|
+
const MAX_DEPTH = 20; // Prevent excessive nesting
|
|
693
702
|
const lastKeyIndex = keyPath.length - 1;
|
|
703
|
+
|
|
704
|
+
if (lastKeyIndex >= MAX_DEPTH) {
|
|
705
|
+
console.warn('Maximum nesting depth exceeded in _deSerialize');
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
694
709
|
for (let i = 0; i < lastKeyIndex; ++i) {
|
|
695
710
|
let key = keyPath[i];
|
|
696
711
|
if (!(key in obj)) obj[key] = {};
|
|
@@ -698,10 +713,13 @@ const _deSerialize = (data) => {
|
|
|
698
713
|
}
|
|
699
714
|
obj[keyPath[lastKeyIndex]] = value;
|
|
700
715
|
}
|
|
716
|
+
|
|
717
|
+
if (!data || typeof data !== 'object') return;
|
|
718
|
+
|
|
701
719
|
for (let datum in data) {
|
|
702
720
|
const matches = datum.match(/\[(.+?)\]/g);
|
|
703
721
|
if (matches) {
|
|
704
|
-
const params = matches.map(function(match) {
|
|
722
|
+
const params = matches.map(function (match) {
|
|
705
723
|
return match.replace(/[[\]]/g, "");
|
|
706
724
|
});
|
|
707
725
|
if (isNaN(params[0])) {
|
|
@@ -765,7 +783,7 @@ const changeUrlParams = (req, key, val) => {
|
|
|
765
783
|
return req.config.url + req.path() + "?" + querystring.stringify(q);
|
|
766
784
|
};
|
|
767
785
|
|
|
768
|
-
const JXP = function(options) {
|
|
786
|
+
const JXP = function (options) {
|
|
769
787
|
const server = restify.createServer();
|
|
770
788
|
const model_dir = options.model_dir || modeldir.findModelDir(path.dirname(process.argv[1]));
|
|
771
789
|
//Set up config with default
|
|
@@ -773,12 +791,12 @@ const JXP = function(options) {
|
|
|
773
791
|
model_dir: path.join(model_dir),
|
|
774
792
|
mongo: options.mongo,
|
|
775
793
|
callbacks: {
|
|
776
|
-
put: function() {},
|
|
777
|
-
post: function() {},
|
|
778
|
-
delete: function() {},
|
|
779
|
-
get: function() {},
|
|
780
|
-
getOne: function() {},
|
|
781
|
-
update: function() {},
|
|
794
|
+
put: function () { },
|
|
795
|
+
post: function () { },
|
|
796
|
+
delete: function () { },
|
|
797
|
+
get: function () { },
|
|
798
|
+
getOne: function () { },
|
|
799
|
+
update: function () { },
|
|
782
800
|
},
|
|
783
801
|
log: "access.log",
|
|
784
802
|
pre_hooks: {
|
|
@@ -841,13 +859,13 @@ const JXP = function(options) {
|
|
|
841
859
|
global.apikey = config.apikey;
|
|
842
860
|
global.server = config.server;
|
|
843
861
|
global.model_dir = model_dir;
|
|
844
|
-
|
|
862
|
+
|
|
845
863
|
// Pre-load models
|
|
846
864
|
var files = fs.readdirSync(config.model_dir);
|
|
847
|
-
let modelnames = files.filter(function(fname) {
|
|
865
|
+
let modelnames = files.filter(function (fname) {
|
|
848
866
|
return fname.indexOf("_model.js") !== -1;
|
|
849
867
|
});
|
|
850
|
-
modelnames.forEach(function(fname) {
|
|
868
|
+
modelnames.forEach(function (fname) {
|
|
851
869
|
var modelname = fname.replace("_model.js", "");
|
|
852
870
|
models[modelname] = require(path.join(config.model_dir, fname));
|
|
853
871
|
});
|
|
@@ -856,9 +874,9 @@ const JXP = function(options) {
|
|
|
856
874
|
security.init(config);
|
|
857
875
|
login.init(config);
|
|
858
876
|
groups.init(config);
|
|
859
|
-
ws.init({models});
|
|
877
|
+
ws.init({ models });
|
|
860
878
|
cache.init(config);
|
|
861
|
-
const docs = new Docs({config, models});
|
|
879
|
+
const docs = new Docs({ config, models });
|
|
862
880
|
|
|
863
881
|
// Set up our API server
|
|
864
882
|
|
|
@@ -877,7 +895,7 @@ const JXP = function(options) {
|
|
|
877
895
|
const cors = corsMiddleware({
|
|
878
896
|
preflightMaxAge: 5, //Optional
|
|
879
897
|
origins: ['*'],
|
|
880
|
-
allowHeaders: ['X-Requested-With','Authorization'],
|
|
898
|
+
allowHeaders: ['X-Requested-With', 'Authorization'],
|
|
881
899
|
exposeHeaders: ['Authorization']
|
|
882
900
|
});
|
|
883
901
|
|
|
@@ -1,40 +1,87 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const mongoose = require("mongoose");
|
|
3
4
|
|
|
4
5
|
const getModelFileContents = (model_dir) => {
|
|
5
6
|
return new Promise((resolve, reject) => {
|
|
6
7
|
try {
|
|
7
|
-
fs.readdir(model_dir, function(err, files) {
|
|
8
|
+
fs.readdir(model_dir, function (err, files) {
|
|
8
9
|
if (err) {
|
|
9
10
|
return reject(err);
|
|
10
11
|
}
|
|
12
|
+
|
|
13
|
+
// Filter for only model files first
|
|
14
|
+
const modelFiles = files.filter(file =>
|
|
15
|
+
file.endsWith('_model.js') &&
|
|
16
|
+
fs.statSync(path.join(model_dir, file)).isFile()
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Limit number of files processed at once
|
|
20
|
+
const MAX_FILES = 1000;
|
|
21
|
+
if (modelFiles.length > MAX_FILES) {
|
|
22
|
+
return reject(new Error(`Too many model files (${modelFiles.length}). Maximum allowed is ${MAX_FILES}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
let models = [];
|
|
12
|
-
|
|
26
|
+
let errors = [];
|
|
27
|
+
|
|
28
|
+
for (const file of modelFiles) {
|
|
13
29
|
const modelname = path.basename(file, ".js").replace("_model", "");
|
|
14
30
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
// Try to get the model from mongoose first
|
|
32
|
+
let modelobj;
|
|
33
|
+
try {
|
|
34
|
+
// Convert modelname to proper case for mongoose (first letter uppercase)
|
|
35
|
+
const modelKey = modelname.charAt(0).toUpperCase() + modelname.slice(1);
|
|
36
|
+
modelobj = mongoose.models[modelKey];
|
|
37
|
+
|
|
38
|
+
// If model doesn't exist in mongoose, try to load it from file
|
|
39
|
+
if (!modelobj) {
|
|
40
|
+
const filePath = path.join(model_dir, file);
|
|
41
|
+
delete require.cache[require.resolve(filePath)];
|
|
42
|
+
modelobj = require(filePath);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// If getting from mongoose fails, try loading from file
|
|
46
|
+
const filePath = path.join(model_dir, file);
|
|
47
|
+
delete require.cache[require.resolve(filePath)];
|
|
48
|
+
modelobj = require(filePath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if we have a valid model with schema and permissions
|
|
52
|
+
if (modelobj && modelobj.schema) {
|
|
53
|
+
const perms = modelobj.schema.get("_perms");
|
|
54
|
+
if (perms && (perms.admin || perms.user || perms.owner || perms.all)) {
|
|
55
|
+
models.push({
|
|
56
|
+
model: modelname,
|
|
57
|
+
file: file,
|
|
58
|
+
perms: perms
|
|
59
|
+
});
|
|
60
|
+
continue; // Skip error handling if successful
|
|
61
|
+
}
|
|
30
62
|
}
|
|
63
|
+
|
|
64
|
+
// If we get here, the model was loaded but didn't have proper schema/perms
|
|
65
|
+
errors.push(`Invalid model structure for ${modelname}`);
|
|
66
|
+
|
|
31
67
|
} catch (error) {
|
|
32
|
-
|
|
68
|
+
// Only add to errors if it's not an OverwriteModelError
|
|
69
|
+
if (!error.message.includes('Cannot overwrite')) {
|
|
70
|
+
errors.push(`Error with model ${modelname}: ${error.message}`);
|
|
71
|
+
console.error(`Error processing model ${modelname}:`, error);
|
|
72
|
+
}
|
|
33
73
|
}
|
|
34
74
|
}
|
|
75
|
+
|
|
76
|
+
// If we have errors but also some valid models, just log the errors
|
|
77
|
+
if (errors.length > 0 && models.length > 0) {
|
|
78
|
+
console.warn('Some models failed to load:', errors);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Return empty array if no models found, but don't treat it as an error
|
|
35
82
|
return resolve(models);
|
|
36
83
|
});
|
|
37
|
-
} catch(err) {
|
|
84
|
+
} catch (err) {
|
|
38
85
|
return reject(err);
|
|
39
86
|
}
|
|
40
87
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jxp",
|
|
3
3
|
"description:": "An opinionated RESTful API library based on Mongoose and Restify. Make an API by just writing Mongoose models.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.15.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "libs/jxp.js",
|
|
7
7
|
"scripts": {
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"url": "https://github.com/WorkSpaceMan/jxp/issues"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"axios": "^1.
|
|
33
|
-
"bcryptjs": "^
|
|
34
|
-
"commander": "^
|
|
35
|
-
"config": "^3.3.
|
|
36
|
-
"dotenv": "^16.4.
|
|
37
|
-
"glob": "
|
|
32
|
+
"axios": "^1.8.4",
|
|
33
|
+
"bcryptjs": "^3.0.2",
|
|
34
|
+
"commander": "^13.1.0",
|
|
35
|
+
"config": "^3.3.12",
|
|
36
|
+
"dotenv": "^16.4.7",
|
|
37
|
+
"glob": "11.0.1",
|
|
38
38
|
"js-yaml": "4.1.0",
|
|
39
39
|
"json2csv": "^5.0.7",
|
|
40
40
|
"jsonwebtoken": "^9.0.2",
|
|
@@ -46,22 +46,22 @@
|
|
|
46
46
|
"mongoose-friendly": "^0.1.4",
|
|
47
47
|
"morgan": "^1.10.0",
|
|
48
48
|
"node-cache": "^5.1.2",
|
|
49
|
-
"nodemailer": "^6.
|
|
49
|
+
"nodemailer": "^6.10.0",
|
|
50
50
|
"nodemailer-smtp-transport": "^2.7.4",
|
|
51
51
|
"path": "^0.12.7",
|
|
52
|
-
"pug": "^3.0.
|
|
52
|
+
"pug": "^3.0.3",
|
|
53
53
|
"querystring": "^0.2.1",
|
|
54
54
|
"rand-token": "^1.0.1",
|
|
55
55
|
"readline-sync": "^1.4.10",
|
|
56
56
|
"restify": "^11.1.0",
|
|
57
57
|
"restify-cors-middleware2": "^2.2.1",
|
|
58
58
|
"restify-errors": "^8.0.2",
|
|
59
|
-
"traverse": "^0.6.
|
|
60
|
-
"underscore": "^1.13.
|
|
61
|
-
"ws": "^8.
|
|
59
|
+
"traverse": "^0.6.11",
|
|
60
|
+
"underscore": "^1.13.7",
|
|
61
|
+
"ws": "^8.18.1"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"chai": "^4.3.
|
|
64
|
+
"chai": "^4.3.10",
|
|
65
65
|
"chai-http": "^4.4.0",
|
|
66
66
|
"mocha": "^10.2.0",
|
|
67
67
|
"moment": "^2.29.4",
|
package/templates/model.pug
CHANGED
|
@@ -36,21 +36,40 @@ block content
|
|
|
36
36
|
th Default
|
|
37
37
|
th Indexed?
|
|
38
38
|
th Unique?
|
|
39
|
+
th Required?
|
|
39
40
|
tbody
|
|
40
41
|
each fieldname in fields
|
|
41
|
-
- let field =
|
|
42
|
+
- let field = safeFields[fieldname]
|
|
42
43
|
if (field.path)
|
|
43
44
|
tr
|
|
44
45
|
td= field.path
|
|
45
|
-
td
|
|
46
|
+
td
|
|
47
|
+
if field.arrayType
|
|
48
|
+
| Array of #{field.arrayType}
|
|
49
|
+
else
|
|
50
|
+
= field.instance
|
|
46
51
|
td= field.options.link
|
|
47
52
|
td= field.options.map_to
|
|
48
53
|
td= field.defaultValue
|
|
49
54
|
td= (field.options.index) ? "x" : ""
|
|
50
55
|
td= (field.options.unique) ? "x" : ""
|
|
56
|
+
td= field.isRequired ? "x" : ""
|
|
51
57
|
hr
|
|
52
|
-
|
|
58
|
+
h2 Field Details
|
|
53
59
|
each fieldname in fields
|
|
54
|
-
- let field =
|
|
55
|
-
|
|
60
|
+
- let field = safeFields[fieldname]
|
|
61
|
+
if field
|
|
62
|
+
.field-details
|
|
63
|
+
h3= field.path
|
|
64
|
+
pre.pre= JSON.stringify(field, null, 2)
|
|
65
|
+
if field.schema
|
|
66
|
+
h4 Nested Schema
|
|
67
|
+
ul
|
|
68
|
+
each nestedField in field.schema
|
|
69
|
+
li #{nestedField.path}: #{nestedField.type}
|
|
70
|
+
if field.validators && field.validators.length
|
|
71
|
+
h4 Validators
|
|
72
|
+
ul
|
|
73
|
+
each validator in field.validators
|
|
74
|
+
li #{validator.type}: #{validator.message}
|
|
56
75
|
|