jxp 2.14.6 → 2.15.1
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 +166 -68
- package/libs/schema_description.js +66 -19
- package/models/test_model.js +13 -12
- package/package.json +14 -14
- package/templates/model.pug +24 -5
- package/test/test.js +531 -440
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");
|
|
@@ -103,6 +103,10 @@ const actionGet = async (req, res) => {
|
|
|
103
103
|
filters = parseFilter(req.query.filter);
|
|
104
104
|
} catch (err) {
|
|
105
105
|
console.trace(new Date(), err);
|
|
106
|
+
// Preserve BadRequestError, convert others to InternalServerError
|
|
107
|
+
if (err instanceof errors.BadRequestError) {
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
106
110
|
throw new errors.InternalServerError(err.toString());
|
|
107
111
|
}
|
|
108
112
|
let search = parseSearch(req.query.search);
|
|
@@ -120,9 +124,9 @@ const actionGet = async (req, res) => {
|
|
|
120
124
|
}
|
|
121
125
|
if (req.query.search) {
|
|
122
126
|
// 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 }});
|
|
127
|
+
q = req.Model.find({ $text: { $search: req.query.search } }, { score: { $meta: "textScore" } }).sort({ score: { $meta: "textScore" } });
|
|
128
|
+
countquery = Object.assign({ $text: { $search: req.query.search } }, countquery);
|
|
129
|
+
qcount = req.Model.find({ $text: { $search: req.query.search } });
|
|
126
130
|
}
|
|
127
131
|
if (res.user) {
|
|
128
132
|
q.options = ({ user: res.user });
|
|
@@ -164,7 +168,7 @@ const actionGet = async (req, res) => {
|
|
|
164
168
|
} else {
|
|
165
169
|
q.populate(req.query.populate);
|
|
166
170
|
}
|
|
167
|
-
result.populate = req.query.populate;
|
|
171
|
+
result.populate = req.query.populate;
|
|
168
172
|
}
|
|
169
173
|
if (req.query.autopopulate) {
|
|
170
174
|
for (let key in req.Model.schema.paths) {
|
|
@@ -189,9 +193,13 @@ const actionGet = async (req, res) => {
|
|
|
189
193
|
result.data = await q.exec();
|
|
190
194
|
res.result = result;
|
|
191
195
|
if (debug) console.timeEnd(opname);
|
|
192
|
-
} catch(err) {
|
|
196
|
+
} catch (err) {
|
|
193
197
|
console.error(new Date(), err);
|
|
194
198
|
if (debug) console.timeEnd(opname);
|
|
199
|
+
// Preserve BadRequestError, convert others to InternalServerError
|
|
200
|
+
if (err instanceof errors.BadRequestError) {
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
195
203
|
if (err.code) throw err;
|
|
196
204
|
throw new errors.InternalServerError(err.toString());
|
|
197
205
|
}
|
|
@@ -204,7 +212,7 @@ const actionGetOne = async (req, res) => {
|
|
|
204
212
|
const data = await getOne(req.Model, req.params.item_id, req.query, { user: res.user });
|
|
205
213
|
res.result = { data };
|
|
206
214
|
if (debug) console.timeEnd(opname);
|
|
207
|
-
} catch(err) {
|
|
215
|
+
} catch (err) {
|
|
208
216
|
console.error(new Date(), err);
|
|
209
217
|
if (debug) console.timeEnd(opname);
|
|
210
218
|
if (err.code) throw err;
|
|
@@ -262,7 +270,7 @@ const actionPut = async (req, res) => {
|
|
|
262
270
|
let silence = req.params._silence;
|
|
263
271
|
if (req.body && req.body._silence) silence = true;
|
|
264
272
|
if (!silence) {
|
|
265
|
-
req.config.callbacks.put.call(null, req.modelname, item, res.user
|
|
273
|
+
req.config.callbacks.put.call(null, req.modelname, item, res.user);
|
|
266
274
|
ws.putHook.call(null, req.modelname, item, res.user);
|
|
267
275
|
}
|
|
268
276
|
res.json({
|
|
@@ -283,12 +291,12 @@ const actionUpdate = async (req, res) => {
|
|
|
283
291
|
const opname = `update ${req.modelname}/${req.params.item_id} ${ops++}`;
|
|
284
292
|
console.time(opname);
|
|
285
293
|
try {
|
|
286
|
-
let body_data =
|
|
294
|
+
let body_data = datamunging.deserialize(req.body);
|
|
287
295
|
const data = await req.Model.update({ _id: req.params.item_id }, body_data);
|
|
288
296
|
let silence = req.params._silence;
|
|
289
297
|
if (req.body && req.body._silence) silence = true;
|
|
290
298
|
if (!silence) {
|
|
291
|
-
req.config.callbacks.put.call(null, req.modelname, data, res.user
|
|
299
|
+
req.config.callbacks.put.call(null, req.modelname, data, res.user);
|
|
292
300
|
ws.putHook.call(null, req.modelname, data, res.user);
|
|
293
301
|
}
|
|
294
302
|
res.json({
|
|
@@ -370,10 +378,10 @@ const actionDelete = async (req, res) => {
|
|
|
370
378
|
}
|
|
371
379
|
res.json({
|
|
372
380
|
status: "ok",
|
|
373
|
-
message: `${req.modelname}/${
|
|
381
|
+
message: `${req.modelname}/${req.params.item_id} deleted`
|
|
374
382
|
});
|
|
375
383
|
if (debug) console.timeEnd(opname);
|
|
376
|
-
} catch(err) {
|
|
384
|
+
} catch (err) {
|
|
377
385
|
console.error(new Date(), err);
|
|
378
386
|
if (debug) console.timeEnd(opname);
|
|
379
387
|
if (err.code) throw err;
|
|
@@ -384,7 +392,7 @@ const actionDelete = async (req, res) => {
|
|
|
384
392
|
const actionCount = async (req, res) => {
|
|
385
393
|
const opname = `count ${req.modelname} ${ops++}`;
|
|
386
394
|
console.time(opname);
|
|
387
|
-
const parseSearch = function(search) {
|
|
395
|
+
const parseSearch = function (search) {
|
|
388
396
|
let result = {};
|
|
389
397
|
for (let i in search) {
|
|
390
398
|
result[i] = new RegExp(search[i], "i");
|
|
@@ -409,7 +417,7 @@ const actionCount = async (req, res) => {
|
|
|
409
417
|
const count = await req.Model.countDocuments(filters).exec();
|
|
410
418
|
res.result = { count };
|
|
411
419
|
if (debug) console.timeEnd(opname);
|
|
412
|
-
} catch(err) {
|
|
420
|
+
} catch (err) {
|
|
413
421
|
console.error(new Date(), err);
|
|
414
422
|
if (debug) console.timeEnd(opname);
|
|
415
423
|
if (err.code) throw err;
|
|
@@ -424,7 +432,7 @@ const actionCall = async (req, res) => {
|
|
|
424
432
|
try {
|
|
425
433
|
const result = await req.Model[req.params.method_name](req.body);
|
|
426
434
|
res.json(result);
|
|
427
|
-
} catch(err) {
|
|
435
|
+
} catch (err) {
|
|
428
436
|
console.error(new Date(), err);
|
|
429
437
|
if (err.code) throw err;
|
|
430
438
|
throw new errors.InternalServerError(err.toString());
|
|
@@ -440,7 +448,7 @@ const actionCallItem = async (req, res) => {
|
|
|
440
448
|
req.params.__user = res.user || null;
|
|
441
449
|
const result = await req.Model[req.params.method_name](item);
|
|
442
450
|
res.json(result);
|
|
443
|
-
} catch(err) {
|
|
451
|
+
} catch (err) {
|
|
444
452
|
console.trace(err);
|
|
445
453
|
if (err.code) throw err;
|
|
446
454
|
throw new errors.InternalServerError(err.toString());
|
|
@@ -493,7 +501,7 @@ const actionQuery = async (req, res) => {
|
|
|
493
501
|
} else {
|
|
494
502
|
q.populate(req.query.populate);
|
|
495
503
|
}
|
|
496
|
-
result.populate = req.query.populate;
|
|
504
|
+
result.populate = req.query.populate;
|
|
497
505
|
}
|
|
498
506
|
if (req.query.autopopulate) {
|
|
499
507
|
for (let key in req.Model.schema.paths) {
|
|
@@ -516,7 +524,7 @@ const actionQuery = async (req, res) => {
|
|
|
516
524
|
res.result = result;
|
|
517
525
|
if (debug) console.timeEnd(opname);
|
|
518
526
|
res.json(result);
|
|
519
|
-
} catch(err) {
|
|
527
|
+
} catch (err) {
|
|
520
528
|
console.error(new Date(), err);
|
|
521
529
|
if (debug) console.timeEnd(opname);
|
|
522
530
|
if (err.code) throw err;
|
|
@@ -611,7 +619,7 @@ const actionBulkWrite = async (req, res) => {
|
|
|
611
619
|
const getOne = async (Model, item_id, params, options) => {
|
|
612
620
|
const query = Model.findById(item_id, {}, options);
|
|
613
621
|
if (params.populate) {
|
|
614
|
-
if ((typeof params.populate === "object")
|
|
622
|
+
if ((typeof params.populate === "object") && !Array.isArray(params.populate)) {
|
|
615
623
|
for (let i in params.populate) {
|
|
616
624
|
query.populate(i, params.populate[i].replace(/,/g, " "));
|
|
617
625
|
}
|
|
@@ -641,56 +649,143 @@ const getOne = async (Model, item_id, params, options) => {
|
|
|
641
649
|
//Don't ever return passwords
|
|
642
650
|
delete item.password;
|
|
643
651
|
return item;
|
|
644
|
-
} catch(err) {
|
|
652
|
+
} catch (err) {
|
|
645
653
|
console.error(err);
|
|
646
654
|
if (err.code) throw err;
|
|
647
655
|
throw new errors.InternalServerError(err.toString());
|
|
648
656
|
}
|
|
649
657
|
};
|
|
650
658
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
659
|
+
// Helper function to check if a string is an ISO date string
|
|
660
|
+
function isISODateString(str) {
|
|
661
|
+
if (typeof str !== 'string') return false;
|
|
662
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
|
|
663
|
+
if (!isoDateRegex.test(str)) return false;
|
|
664
|
+
const date = new Date(str);
|
|
665
|
+
if (isNaN(date.getTime())) {
|
|
666
|
+
throw new errors.BadRequestError("Invalid date format");
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const parseFilter = (filter, depth = 0) => {
|
|
672
|
+
const MAX_DEPTH = 10;
|
|
673
|
+
|
|
674
|
+
if (!filter) return {};
|
|
675
|
+
if (depth > MAX_DEPTH) {
|
|
676
|
+
console.warn('Maximum filter depth exceeded');
|
|
677
|
+
return filter;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (typeof filter !== "object" || filter === null) return filter;
|
|
681
|
+
|
|
682
|
+
// Handle arrays by merging their operators
|
|
683
|
+
if (Array.isArray(filter)) {
|
|
684
|
+
const result = {};
|
|
685
|
+
filter.forEach(item => {
|
|
686
|
+
if (typeof item === "string" && item.includes(":")) {
|
|
687
|
+
const parts = item.split(":");
|
|
688
|
+
const key = parts[0];
|
|
689
|
+
const value = parts.slice(1).join(":");
|
|
690
|
+
if (key.startsWith("$")) {
|
|
691
|
+
try {
|
|
692
|
+
if (isISODateString(value)) {
|
|
693
|
+
result[key] = new Date(value);
|
|
694
|
+
} else {
|
|
695
|
+
result[key] = value;
|
|
696
|
+
}
|
|
697
|
+
} catch (err) {
|
|
698
|
+
if (err instanceof errors.BadRequestError) {
|
|
699
|
+
throw err;
|
|
700
|
+
}
|
|
701
|
+
throw new errors.BadRequestError("Invalid date format");
|
|
674
702
|
}
|
|
675
703
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Create a new object to avoid modifying the input
|
|
710
|
+
const parsedFilter = {};
|
|
711
|
+
|
|
712
|
+
for (let i in filter) {
|
|
713
|
+
if (filter[i] === "false") {
|
|
714
|
+
parsedFilter[i] = false;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (filter[i] === "true") {
|
|
718
|
+
parsedFilter[i] = true;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (typeof filter[i] === "string") {
|
|
722
|
+
try {
|
|
723
|
+
if (isISODateString(filter[i])) {
|
|
724
|
+
parsedFilter[i] = new Date(filter[i]);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
} catch (err) {
|
|
728
|
+
if (err instanceof errors.BadRequestError) {
|
|
729
|
+
throw err;
|
|
730
|
+
}
|
|
731
|
+
throw new errors.BadRequestError("Invalid date format");
|
|
732
|
+
}
|
|
733
|
+
if (filter[i].includes(":")) {
|
|
734
|
+
const parts = filter[i].split(":");
|
|
735
|
+
const key = parts[0];
|
|
736
|
+
const value = parts.slice(1).join(":");
|
|
737
|
+
if (key.startsWith("$")) {
|
|
738
|
+
try {
|
|
739
|
+
if (isISODateString(value)) {
|
|
740
|
+
if (!parsedFilter[i]) parsedFilter[i] = {};
|
|
741
|
+
parsedFilter[i][key] = new Date(value);
|
|
742
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
743
|
+
if (!parsedFilter[i]) parsedFilter[i] = {};
|
|
744
|
+
parsedFilter[i][key] = value.slice(1, -1).split(",");
|
|
745
|
+
} else if (key === "$regex" && value.startsWith("/")) {
|
|
746
|
+
if (!parsedFilter[i]) parsedFilter[i] = {};
|
|
747
|
+
const match = value.match(/^\/(.+?)\/([gimy]*)$/);
|
|
748
|
+
if (match) {
|
|
749
|
+
parsedFilter[i][key] = new RegExp(match[1], match[2]);
|
|
750
|
+
}
|
|
751
|
+
} else {
|
|
752
|
+
if (!parsedFilter[i]) parsedFilter[i] = {};
|
|
753
|
+
parsedFilter[i][key] = value;
|
|
754
|
+
}
|
|
755
|
+
} catch (err) {
|
|
756
|
+
if (err instanceof errors.BadRequestError) {
|
|
757
|
+
throw err;
|
|
758
|
+
}
|
|
759
|
+
throw new errors.BadRequestError("Invalid date format");
|
|
682
760
|
}
|
|
761
|
+
} else {
|
|
762
|
+
parsedFilter[i] = filter[i];
|
|
683
763
|
}
|
|
764
|
+
} else {
|
|
765
|
+
parsedFilter[i] = filter[i];
|
|
684
766
|
}
|
|
685
|
-
})
|
|
767
|
+
} else if (Array.isArray(filter[i])) {
|
|
768
|
+
parsedFilter[i] = parseFilter(filter[i], depth + 1);
|
|
769
|
+
} else if (typeof filter[i] === "object") {
|
|
770
|
+
parsedFilter[i] = parseFilter(filter[i], depth + 1);
|
|
771
|
+
} else {
|
|
772
|
+
parsedFilter[i] = filter[i];
|
|
773
|
+
}
|
|
686
774
|
}
|
|
687
|
-
|
|
688
|
-
|
|
775
|
+
|
|
776
|
+
return parsedFilter;
|
|
777
|
+
};
|
|
689
778
|
|
|
690
779
|
const _deSerialize = (data) => {
|
|
691
780
|
function assign(obj, keyPath, value) {
|
|
692
|
-
//
|
|
781
|
+
const MAX_DEPTH = 20; // Prevent excessive nesting
|
|
693
782
|
const lastKeyIndex = keyPath.length - 1;
|
|
783
|
+
|
|
784
|
+
if (lastKeyIndex >= MAX_DEPTH) {
|
|
785
|
+
console.warn('Maximum nesting depth exceeded in _deSerialize');
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
694
789
|
for (let i = 0; i < lastKeyIndex; ++i) {
|
|
695
790
|
let key = keyPath[i];
|
|
696
791
|
if (!(key in obj)) obj[key] = {};
|
|
@@ -698,10 +793,13 @@ const _deSerialize = (data) => {
|
|
|
698
793
|
}
|
|
699
794
|
obj[keyPath[lastKeyIndex]] = value;
|
|
700
795
|
}
|
|
796
|
+
|
|
797
|
+
if (!data || typeof data !== 'object') return;
|
|
798
|
+
|
|
701
799
|
for (let datum in data) {
|
|
702
800
|
const matches = datum.match(/\[(.+?)\]/g);
|
|
703
801
|
if (matches) {
|
|
704
|
-
const params = matches.map(function(match) {
|
|
802
|
+
const params = matches.map(function (match) {
|
|
705
803
|
return match.replace(/[[\]]/g, "");
|
|
706
804
|
});
|
|
707
805
|
if (isNaN(params[0])) {
|
|
@@ -765,7 +863,7 @@ const changeUrlParams = (req, key, val) => {
|
|
|
765
863
|
return req.config.url + req.path() + "?" + querystring.stringify(q);
|
|
766
864
|
};
|
|
767
865
|
|
|
768
|
-
const JXP = function(options) {
|
|
866
|
+
const JXP = function (options) {
|
|
769
867
|
const server = restify.createServer();
|
|
770
868
|
const model_dir = options.model_dir || modeldir.findModelDir(path.dirname(process.argv[1]));
|
|
771
869
|
//Set up config with default
|
|
@@ -773,12 +871,12 @@ const JXP = function(options) {
|
|
|
773
871
|
model_dir: path.join(model_dir),
|
|
774
872
|
mongo: options.mongo,
|
|
775
873
|
callbacks: {
|
|
776
|
-
put: function() {},
|
|
777
|
-
post: function() {},
|
|
778
|
-
delete: function() {},
|
|
779
|
-
get: function() {},
|
|
780
|
-
getOne: function() {},
|
|
781
|
-
update: function() {},
|
|
874
|
+
put: function () { },
|
|
875
|
+
post: function () { },
|
|
876
|
+
delete: function () { },
|
|
877
|
+
get: function () { },
|
|
878
|
+
getOne: function () { },
|
|
879
|
+
update: function () { },
|
|
782
880
|
},
|
|
783
881
|
log: "access.log",
|
|
784
882
|
pre_hooks: {
|
|
@@ -841,13 +939,13 @@ const JXP = function(options) {
|
|
|
841
939
|
global.apikey = config.apikey;
|
|
842
940
|
global.server = config.server;
|
|
843
941
|
global.model_dir = model_dir;
|
|
844
|
-
|
|
942
|
+
|
|
845
943
|
// Pre-load models
|
|
846
944
|
var files = fs.readdirSync(config.model_dir);
|
|
847
|
-
let modelnames = files.filter(function(fname) {
|
|
945
|
+
let modelnames = files.filter(function (fname) {
|
|
848
946
|
return fname.indexOf("_model.js") !== -1;
|
|
849
947
|
});
|
|
850
|
-
modelnames.forEach(function(fname) {
|
|
948
|
+
modelnames.forEach(function (fname) {
|
|
851
949
|
var modelname = fname.replace("_model.js", "");
|
|
852
950
|
models[modelname] = require(path.join(config.model_dir, fname));
|
|
853
951
|
});
|
|
@@ -856,9 +954,9 @@ const JXP = function(options) {
|
|
|
856
954
|
security.init(config);
|
|
857
955
|
login.init(config);
|
|
858
956
|
groups.init(config);
|
|
859
|
-
ws.init({models});
|
|
957
|
+
ws.init({ models });
|
|
860
958
|
cache.init(config);
|
|
861
|
-
const docs = new Docs({config, models});
|
|
959
|
+
const docs = new Docs({ config, models });
|
|
862
960
|
|
|
863
961
|
// Set up our API server
|
|
864
962
|
|
|
@@ -877,7 +975,7 @@ const JXP = function(options) {
|
|
|
877
975
|
const cors = corsMiddleware({
|
|
878
976
|
preflightMaxAge: 5, //Optional
|
|
879
977
|
origins: ['*'],
|
|
880
|
-
allowHeaders: ['X-Requested-With','Authorization'],
|
|
978
|
+
allowHeaders: ['X-Requested-With', 'Authorization'],
|
|
881
979
|
exposeHeaders: ['Authorization']
|
|
882
980
|
});
|
|
883
981
|
|
|
@@ -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/models/test_model.js
CHANGED
|
@@ -11,30 +11,31 @@ const TestSchema = new JXPSchema({
|
|
|
11
11
|
fulltext: { type: String, index: { text: true } },
|
|
12
12
|
link_id: { type: ObjectId, link: "Link", }, // We can populate these links during a query
|
|
13
13
|
other_link_id: { type: ObjectId, link: "Link", map_to: "other_link" },
|
|
14
|
-
array_link_id: [{ type: ObjectId, link: "Link", map_to: "array_link", justOne: false }
|
|
15
|
-
composite_array: [
|
|
14
|
+
array_link_id: [{ type: ObjectId, link: "Link", map_to: "array_link", justOne: false }],
|
|
15
|
+
composite_array: [
|
|
16
16
|
{ afoo: String, abar: Number }
|
|
17
|
-
]
|
|
17
|
+
],
|
|
18
|
+
date_field: { type: Date, index: true } // Added date field for testing date filtering
|
|
18
19
|
},
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
{
|
|
21
|
+
perms: {
|
|
22
|
+
admin: "crud", // CRUD = Create, Retrieve, Update and Delete
|
|
23
|
+
owner: "crud",
|
|
24
|
+
user: "cr",
|
|
25
|
+
all: "r" // Unauthenticated users will be able to read from test, but that is all
|
|
26
|
+
}
|
|
25
27
|
}
|
|
26
|
-
}
|
|
27
28
|
);
|
|
28
29
|
|
|
29
30
|
// Full text index
|
|
30
31
|
// TestSchema.index( { "$**": "text" } );
|
|
31
32
|
|
|
32
33
|
// We can define useful functions that we can call through the API using our /call endpoint
|
|
33
|
-
TestSchema.statics.test = function() {
|
|
34
|
+
TestSchema.statics.test = function () {
|
|
34
35
|
return "Testing OKAY!";
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
TestSchema.pre("save", function(next) {
|
|
38
|
+
TestSchema.pre("save", function (next) {
|
|
38
39
|
// If we have the setting "error" set to true, we will throw an error
|
|
39
40
|
if (this.bar == "Throw an error") {
|
|
40
41
|
throw new errors.ImATeapotError("I'm a teapot");
|
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.1",
|
|
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,25 +46,25 @@
|
|
|
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",
|
|
68
68
|
"should": "^13.2.3"
|
|
69
69
|
}
|
|
70
|
-
}
|
|
70
|
+
}
|