jxp 2.14.5 → 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/bin/server.js CHANGED
@@ -78,6 +78,7 @@ let port = process.env.NODE_DOCKER_PORT || process.env.PORT || config.port || 40
78
78
  if (process.env.NODE_ENV === "test") port = 4005;
79
79
  server.listen(port, function() {
80
80
  console.log('%s listening at %s', `${pkg.name} v${pkg.version}`, server.url);
81
+ console.log(`Mongoose version ${mongoose.version}`);
81
82
  });
82
83
 
83
84
  module.exports = server; // For testing
@@ -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 = { due_date: '2016-02-01T00:00:00Z',
45
- from_document: '',
46
- allow_online_payment: 'false',
47
- paid: 'true',
48
- locked: 'false',
49
- customer_id: '2446859',
50
- modified: '2016-02-02T15:09:06.983',
51
- created: '2016-02-01T21:09:37.873',
52
- date: '2016-02-01T00:00:00Z',
53
- inclusive: 'false',
54
- discount_percentage: '0',
55
- tax_reference: '',
56
- discount: '0',
57
- amount_due: '0',
58
- printed: 'false',
59
- editable: 'true',
60
- has_attachments: 'false',
61
- has_notes: 'false',
62
- has_anticipated_date: 'false',
63
- lines:
64
- [ { SelectionId: '6824935',
65
- TaxTypeId: '965302',
66
- ID: '74465675',
67
- LineType: '0',
68
- Quantity: '1',
69
- Unit: '',
70
- TaxPercentage: '0.14',
71
- DiscountPercentage: '0',
72
- Discount: '0' },
73
- { ID: '74465676',
74
- LineType: '0',
75
- Quantity: '4',
76
- Unit: '',
77
- TaxPercentage: '0.14',
78
- DiscountPercentage: '0',
79
- Discount: '0',
80
- Comments: '' } ],
81
- location: '547d506b184d110c0601314c',
82
- id: '93490358' }
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
- var parentRE = /#{\s*?_\s*?}/,
166
- slice = Array.prototype.slice;
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
- _.each(slice.call(arguments, 1), function(source) {
169
- for (var prop in source) {
170
- if (_.isUndefined(obj[prop]) || _.isFunction(obj[prop]) || _.isNull(source[prop]) || _.isDate(source[prop])) {
171
- obj[prop] = source[prop];
172
- }
173
- else if (_.isString(source[prop]) && parentRE.test(source[prop])) {
174
- if (_.isString(obj[prop])) {
175
- obj[prop] = source[prop].replace(parentRE, obj[prop]);
176
- }
177
- }
178
- else if (_.isArray(obj[prop]) || _.isArray(source[prop])){
179
- if (!_.isArray(obj[prop]) || !_.isArray(source[prop])){
180
- throw new Error('Trying to combine an array with a non-array (' + prop + ')');
181
- } else {
182
- obj[prop] = _.reject(_.deepExtend(_.clone(obj[prop]), source[prop]), function (item) { return _.isNull(item);});
183
- }
184
- }
185
- else if (_.isObject(obj[prop]) || _.isObject(source[prop])){
186
- if (!_.isObject(obj[prop]) || !_.isObject(source[prop])){
187
- throw new Error('Trying to combine an object with a non-object (' + prop + ')');
188
- } else {
189
- obj[prop] = _.deepExtend(_.clone(obj[prop]), source[prop]);
190
- }
191
- } else {
192
- obj[prop] = source[prop];
193
- }
194
- }
195
- });
196
- return obj;
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
- console.dir(model.schema.opts);
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
- this.renderTemplate(res, "model", { model, fields, perms });
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 : { $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 }});
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 = datamunging.deserialize(req.body);
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}/${ req.params.item_id } deleted`
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") && !Array.isArray(params.populate)) {
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
- if (!filter)
653
- return {};
654
- if (typeof filter == "object") {
655
- Object.keys(filter).forEach(function(key) {
656
- var val = filter[key];
657
- if (filter[key] === "false") filter[key] = false;
658
- if (filter[key] === "true") filter[key] = true;
659
- if (val.indexOf) {
660
- if (val.indexOf(":") !== -1) {
661
- let tmp = val.split(":");
662
- filter[key] = {};
663
- let tmpkey = tmp.shift();
664
- let tmpval = tmp.join(":");
665
- if ((tmpval[0] === "[") && (tmpval[tmpval.length - 1] === "]")) { // Could be an array for a $in or similar
666
- let arr = tmpval.slice(1, tmpval.length - 1).split(",");
667
- tmpval = arr;
668
- }
669
- filter[key][tmpkey] = tmpval;
670
- if (tmpkey === "$regex" && tmpval[0] === "/") {
671
- let match = tmpval.match(new RegExp('^/(.*?)/([gimy]*)$'));
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
- if (typeof val == "object") {
677
- let result = parseFilter(val);
684
+ }
685
+ if (typeof val == "object") {
686
+ let result = parseFilter(val, depth + 1);
687
+ if (result && typeof result === 'object') {
678
688
  filter[key] = {};
679
- for (let x = 0; x < result.length; x++) {
680
- filter[key][Object.keys(result[x])[0]] =
681
- result[x][Object.keys(result[x])[0]];
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
- // http://stackoverflow.com/questions/5484673/javascript-how-to-dynamically-create-nested-objects-using-object-names-given-by
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
- for (const file of files) {
26
+ let errors = [];
27
+
28
+ for (const file of modelFiles) {
13
29
  const modelname = path.basename(file, ".js").replace("_model", "");
14
30
  try {
15
- const modelobj = require(path.join(model_dir, file));
16
- if (
17
- modelobj.schema &&
18
- modelobj.schema.get("_perms") &&
19
- (modelobj.schema.get("_perms").admin ||
20
- modelobj.schema.get("_perms").user ||
21
- modelobj.schema.get("_perms").owner ||
22
- modelobj.schema.get("_perms").all)
23
- ) {
24
- let model = {
25
- model: modelname,
26
- file: file,
27
- perms: modelobj.schema.get("_perms")
28
- };
29
- models.push(model);
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
- console.error("Error with model " + modelname, error);
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.14.5",
4
+ "version": "2.15.0",
5
5
  "private": false,
6
6
  "main": "libs/jxp.js",
7
7
  "scripts": {
@@ -29,39 +29,39 @@
29
29
  "url": "https://github.com/WorkSpaceMan/jxp/issues"
30
30
  },
31
31
  "dependencies": {
32
- "axios": "^1.6.2",
33
- "bcryptjs": "^2.4.3",
34
- "commander": "^11.1.0",
35
- "config": "^3.3.9",
36
- "dotenv": "^16.3.1",
37
- "glob": "8.1.0",
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
- "jsonwebtoken": "^9.0.1",
40
+ "jsonwebtoken": "^9.0.2",
41
41
  "jstransformer-markdown-it": "^3.0.0",
42
42
  "jxp-helper": "^1.4.3",
43
43
  "mkdirp": "^3.0.1",
44
- "moment": "^2.29.4",
45
- "mongoose": "6.8.4",
44
+ "moment": "^2.30.1",
45
+ "mongoose": "6.12.6",
46
46
  "mongoose-friendly": "^0.1.4",
47
47
  "morgan": "^1.10.0",
48
48
  "node-cache": "^5.1.2",
49
- "nodemailer": "^6.9.3",
49
+ "nodemailer": "^6.10.0",
50
50
  "nodemailer-smtp-transport": "^2.7.4",
51
51
  "path": "^0.12.7",
52
- "pug": "^3.0.2",
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.7",
60
- "underscore": "^1.13.6",
61
- "ws": "^8.15.1"
59
+ "traverse": "^0.6.11",
60
+ "underscore": "^1.13.7",
61
+ "ws": "^8.18.1"
62
62
  },
63
63
  "devDependencies": {
64
- "chai": "^4.3.7",
64
+ "chai": "^4.3.10",
65
65
  "chai-http": "^4.4.0",
66
66
  "mocha": "^10.2.0",
67
67
  "moment": "^2.29.4",
@@ -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 = model.schema.paths[fieldname]
42
+ - let field = safeFields[fieldname]
42
43
  if (field.path)
43
44
  tr
44
45
  td= field.path
45
- td= field.instance
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
- h1 Raw definition
58
+ h2 Field Details
53
59
  each fieldname in fields
54
- - let field = model.schema.paths[fieldname]
55
- pre.pre= JSON.stringify(field, null, "\t")
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