jxp 3.0.0 → 4.0.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.
Files changed (219) hide show
  1. package/.env.sample +31 -6
  2. package/README.md +27 -13
  3. package/dist/bin/init_db.d.ts +3 -0
  4. package/dist/bin/init_db.d.ts.map +1 -0
  5. package/dist/bin/init_db.js +48 -0
  6. package/dist/bin/init_db.js.map +1 -0
  7. package/dist/bin/server.d.ts +4 -0
  8. package/dist/bin/server.d.ts.map +1 -0
  9. package/dist/bin/server.js +132 -0
  10. package/dist/bin/server.js.map +1 -0
  11. package/dist/bin/setup.d.ts +3 -0
  12. package/dist/bin/setup.d.ts.map +1 -0
  13. package/dist/bin/setup.js +185 -0
  14. package/dist/bin/setup.js.map +1 -0
  15. package/dist/globals.d.ts +14 -0
  16. package/dist/globals.d.ts.map +1 -0
  17. package/dist/globals.js +3 -0
  18. package/dist/globals.js.map +1 -0
  19. package/dist/libs/aggregate_guard.d.ts +5 -0
  20. package/dist/libs/aggregate_guard.d.ts.map +1 -0
  21. package/dist/libs/aggregate_guard.js +58 -0
  22. package/dist/libs/aggregate_guard.js.map +1 -0
  23. package/dist/libs/bulkwrite_guard.d.ts +5 -0
  24. package/dist/libs/bulkwrite_guard.d.ts.map +1 -0
  25. package/dist/libs/bulkwrite_guard.js +44 -0
  26. package/dist/libs/bulkwrite_guard.js.map +1 -0
  27. package/dist/libs/cache.d.ts +2 -0
  28. package/dist/libs/cache.d.ts.map +1 -0
  29. package/dist/libs/cache.js +94 -0
  30. package/dist/libs/cache.js.map +1 -0
  31. package/dist/libs/call_guard.d.ts +3 -0
  32. package/dist/libs/call_guard.d.ts.map +1 -0
  33. package/dist/libs/call_guard.js +17 -0
  34. package/dist/libs/call_guard.js.map +1 -0
  35. package/dist/libs/connection_string.d.ts +3 -0
  36. package/dist/libs/connection_string.d.ts.map +1 -0
  37. package/dist/libs/connection_string.js +4 -0
  38. package/dist/libs/connection_string.js.map +1 -0
  39. package/dist/libs/datamunging.d.ts +2 -0
  40. package/dist/libs/datamunging.d.ts.map +1 -0
  41. package/dist/libs/datamunging.js +320 -0
  42. package/dist/libs/datamunging.js.map +1 -0
  43. package/dist/libs/docs.d.ts +2 -0
  44. package/dist/libs/docs.d.ts.map +1 -0
  45. package/dist/libs/docs.js +307 -0
  46. package/dist/libs/docs.js.map +1 -0
  47. package/dist/libs/groups.d.ts +2 -0
  48. package/dist/libs/groups.d.ts.map +1 -0
  49. package/dist/libs/groups.js +136 -0
  50. package/dist/libs/groups.js.map +1 -0
  51. package/dist/libs/jxp.d.ts +4 -0
  52. package/dist/libs/jxp.d.ts.map +1 -0
  53. package/dist/libs/jxp.js +1157 -0
  54. package/dist/libs/jxp.js.map +1 -0
  55. package/dist/libs/link_index.d.ts +14 -0
  56. package/dist/libs/link_index.d.ts.map +1 -0
  57. package/dist/libs/link_index.js +37 -0
  58. package/dist/libs/link_index.js.map +1 -0
  59. package/dist/libs/load-config.d.ts +16 -0
  60. package/dist/libs/load-config.d.ts.map +1 -0
  61. package/dist/libs/load-config.js +155 -0
  62. package/dist/libs/load-config.js.map +1 -0
  63. package/dist/libs/login.d.ts +2 -0
  64. package/dist/libs/login.d.ts.map +1 -0
  65. package/dist/libs/login.js +244 -0
  66. package/dist/libs/login.js.map +1 -0
  67. package/dist/libs/modeldir.d.ts +5 -0
  68. package/dist/libs/modeldir.d.ts.map +1 -0
  69. package/dist/libs/modeldir.js +15 -0
  70. package/dist/libs/modeldir.js.map +1 -0
  71. package/dist/libs/query_limits.d.ts +2 -0
  72. package/dist/libs/query_limits.d.ts.map +1 -0
  73. package/dist/libs/query_limits.js +84 -0
  74. package/dist/libs/query_limits.js.map +1 -0
  75. package/dist/libs/query_manipulation.d.ts +3 -0
  76. package/dist/libs/query_manipulation.d.ts.map +1 -0
  77. package/{libs → dist/libs}/query_manipulation.js +22 -21
  78. package/dist/libs/query_manipulation.js.map +1 -0
  79. package/dist/libs/query_sanitize.d.ts +5 -0
  80. package/dist/libs/query_sanitize.d.ts.map +1 -0
  81. package/dist/libs/query_sanitize.js +109 -0
  82. package/dist/libs/query_sanitize.js.map +1 -0
  83. package/dist/libs/response_sanitize.d.ts +7 -0
  84. package/dist/libs/response_sanitize.d.ts.map +1 -0
  85. package/dist/libs/response_sanitize.js +57 -0
  86. package/dist/libs/response_sanitize.js.map +1 -0
  87. package/dist/libs/safe_error.d.ts +3 -0
  88. package/dist/libs/safe_error.d.ts.map +1 -0
  89. package/dist/libs/safe_error.js +14 -0
  90. package/dist/libs/safe_error.js.map +1 -0
  91. package/dist/libs/schema.d.ts +14 -0
  92. package/dist/libs/schema.d.ts.map +1 -0
  93. package/dist/libs/schema.js +98 -0
  94. package/dist/libs/schema.js.map +1 -0
  95. package/dist/libs/schema_description.d.ts +3 -0
  96. package/dist/libs/schema_description.d.ts.map +1 -0
  97. package/{libs → dist/libs}/schema_description.js +12 -21
  98. package/dist/libs/schema_description.js.map +1 -0
  99. package/dist/libs/security.d.ts +2 -0
  100. package/dist/libs/security.d.ts.map +1 -0
  101. package/dist/libs/security.js +428 -0
  102. package/dist/libs/security.js.map +1 -0
  103. package/dist/libs/setup.d.ts +2 -0
  104. package/dist/libs/setup.d.ts.map +1 -0
  105. package/dist/libs/setup.js +87 -0
  106. package/dist/libs/setup.js.map +1 -0
  107. package/dist/libs/startup-deprecations.d.ts +2 -0
  108. package/dist/libs/startup-deprecations.d.ts.map +1 -0
  109. package/dist/libs/startup-deprecations.js +21 -0
  110. package/dist/libs/startup-deprecations.js.map +1 -0
  111. package/dist/libs/startup.d.ts +16 -0
  112. package/dist/libs/startup.d.ts.map +1 -0
  113. package/dist/libs/startup.js +63 -0
  114. package/dist/libs/startup.js.map +1 -0
  115. package/dist/libs/ws.d.ts +2 -0
  116. package/dist/libs/ws.d.ts.map +1 -0
  117. package/{libs → dist/libs}/ws.js +91 -73
  118. package/dist/libs/ws.js.map +1 -0
  119. package/dist/models/apikey_model.d.ts +10 -0
  120. package/dist/models/apikey_model.d.ts.map +1 -0
  121. package/dist/models/apikey_model.js +14 -0
  122. package/dist/models/apikey_model.js.map +1 -0
  123. package/dist/models/link_model.d.ts +9 -0
  124. package/dist/models/link_model.d.ts.map +1 -0
  125. package/dist/models/link_model.js +20 -0
  126. package/dist/models/link_model.js.map +1 -0
  127. package/dist/models/refreshtoken_model.d.ts +11 -0
  128. package/dist/models/refreshtoken_model.d.ts.map +1 -0
  129. package/dist/models/refreshtoken_model.js +22 -0
  130. package/dist/models/refreshtoken_model.js.map +1 -0
  131. package/dist/models/test_model.d.ts +24 -0
  132. package/dist/models/test_model.d.ts.map +1 -0
  133. package/dist/models/test_model.js +44 -0
  134. package/dist/models/test_model.js.map +1 -0
  135. package/dist/models/token_model.d.ts +14 -0
  136. package/dist/models/token_model.d.ts.map +1 -0
  137. package/dist/models/token_model.js +25 -0
  138. package/dist/models/token_model.js.map +1 -0
  139. package/dist/models/user_model.d.ts +15 -0
  140. package/dist/models/user_model.d.ts.map +1 -0
  141. package/dist/models/user_model.js +38 -0
  142. package/dist/models/user_model.js.map +1 -0
  143. package/dist/models/usergroups_model.d.ts +9 -0
  144. package/dist/models/usergroups_model.d.ts.map +1 -0
  145. package/dist/models/usergroups_model.js +18 -0
  146. package/dist/models/usergroups_model.js.map +1 -0
  147. package/dist/types/jxp-config.d.ts +134 -0
  148. package/dist/types/jxp-config.d.ts.map +1 -0
  149. package/dist/types/jxp-config.js +3 -0
  150. package/dist/types/jxp-config.js.map +1 -0
  151. package/dist/types/schema-fields.d.ts +43 -0
  152. package/dist/types/schema-fields.d.ts.map +1 -0
  153. package/dist/types/schema-fields.js +3 -0
  154. package/dist/types/schema-fields.js.map +1 -0
  155. package/docs/aggregations.md +40 -21
  156. package/docs/api.md +55 -15
  157. package/docs/authentication.md +60 -37
  158. package/docs/bulk_writes.md +11 -8
  159. package/docs/caching.md +28 -11
  160. package/docs/changelog.md +237 -31
  161. package/docs/configuration.md +99 -118
  162. package/docs/hooks.md +12 -8
  163. package/docs/index.md +17 -17
  164. package/docs/installation.md +23 -18
  165. package/docs/permissions.md +33 -19
  166. package/docs/queries.md +9 -5
  167. package/docs/schemas.md +21 -10
  168. package/docs/special.md +4 -4
  169. package/docs/typescript.md +86 -0
  170. package/docs/websocket.md +99 -1
  171. package/mkdocs.yml +1 -0
  172. package/package.json +121 -69
  173. package/templates/api.pug +32 -0
  174. package/templates/app-server.js +43 -0
  175. package/templates/app.env.sample +8 -0
  176. package/templates/assets/api-console.js +133 -0
  177. package/templates/assets/docs.css +227 -0
  178. package/templates/index.pug +62 -2
  179. package/templates/layout.pug +17 -11
  180. package/templates/md.pug +1 -1
  181. package/templates/model.pug +117 -68
  182. package/templates/navbar.pug +14 -21
  183. package/templates/sidebar.pug +30 -0
  184. package/.env +0 -9
  185. package/.eslintrc.json +0 -20
  186. package/.jshintrc +0 -4
  187. package/.vscode/launch.json +0 -34
  188. package/.vscode/settings.json +0 -3
  189. package/Dockerfile +0 -7
  190. package/bin/init_db.js +0 -54
  191. package/bin/server.js +0 -131
  192. package/bin/setup.js +0 -190
  193. package/config_sample.json +0 -10
  194. package/docker-compose.yml +0 -32
  195. package/libs/cache.js +0 -92
  196. package/libs/connection_string.js +0 -24
  197. package/libs/datamunging.js +0 -335
  198. package/libs/docs.js +0 -162
  199. package/libs/groups.js +0 -133
  200. package/libs/jxp.js +0 -1208
  201. package/libs/login.js +0 -222
  202. package/libs/modeldir.js +0 -14
  203. package/libs/schema.js +0 -99
  204. package/libs/security.js +0 -410
  205. package/libs/setup.js +0 -90
  206. package/models/apikey_model.js +0 -10
  207. package/models/link_model.js +0 -17
  208. package/models/refreshtoken_model.js +0 -20
  209. package/models/test_model.js +0 -50
  210. package/models/token_model.js +0 -23
  211. package/models/user_model.js +0 -38
  212. package/models/usergroups_model.js +0 -15
  213. package/setup.sh +0 -3
  214. package/test/.eslintrc.json +0 -13
  215. package/test/auth.js +0 -190
  216. package/test/init.js +0 -92
  217. package/test/setup.test.js +0 -62
  218. package/test/test.js +0 -1527
  219. package/test/wstest.js.old +0 -124
@@ -0,0 +1,1157 @@
1
+ "use strict";
2
+ const errors = require("restify-errors");
3
+ const restify = require("restify");
4
+ const path = require("path");
5
+ const security = require("./security");
6
+ const datamunging = require("./datamunging");
7
+ const login = require("./login");
8
+ const groups = require("./groups");
9
+ const setup = require("./setup");
10
+ const Docs = require("./docs");
11
+ const querystring = require("node:querystring");
12
+ const fs = require("fs");
13
+ const morgan = require("morgan");
14
+ const ws = require("./ws");
15
+ const modeldir = require("./modeldir");
16
+ const query_manipulation = require("./query_manipulation");
17
+ const corsMiddleware = require('restify-cors-middleware2');
18
+ const { Parser: CsvParser } = require('@json2csv/plainjs');
19
+ const cache = require("./cache");
20
+ const query_limits = require("./query_limits");
21
+ const query_sanitize = require("./query_sanitize");
22
+ const aggregate_guard = require("./aggregate_guard");
23
+ const bulkwrite_guard = require("./bulkwrite_guard");
24
+ const call_guard = require("./call_guard");
25
+ const response_sanitize = require("./response_sanitize");
26
+ const link_index = require("./link_index");
27
+ const { safeErrorMessage } = require("./safe_error");
28
+ const schemaModule = require("./schema");
29
+ global.JXPSchema = schemaModule.default || schemaModule;
30
+ var models = {};
31
+ var ops = 0;
32
+ var debug = false;
33
+ const USER_PRIVILEGE_FIELDS = ["admin", "password", "groups"];
34
+ function getStripFields(req) {
35
+ return req.config?.security?.strip_fields || ["password"];
36
+ }
37
+ function getSecurityOpts(req) {
38
+ return req.config?.security || {};
39
+ }
40
+ function advancedQueryAllowed(Model, kind) {
41
+ const opts = Model.schema.opts;
42
+ const aq = opts?.advanced_queries;
43
+ if (kind === "bulkwrite") {
44
+ return aq?.bulkwrite === true;
45
+ }
46
+ if (kind === "aggregate") {
47
+ return aq?.aggregate !== false;
48
+ }
49
+ return aq?.query !== false;
50
+ }
51
+ // Middleware
52
+ const middlewareModel = (req, res, next) => {
53
+ const modelname = req.params.modelname;
54
+ req.modelname = modelname;
55
+ req.Model = models[modelname];
56
+ if (!req.Model) {
57
+ throw new errors.NotFoundError(`Model ${modelname} not found`);
58
+ }
59
+ return next();
60
+ };
61
+ const middlewarePasswords = (req, res, next) => {
62
+ if (req.body && req.body.password) {
63
+ if (req.query.password_override) {
64
+ if (!res.user?.admin) {
65
+ throw new errors.ForbiddenError("password_override requires admin");
66
+ }
67
+ }
68
+ else {
69
+ req.body.password = security.encPassword(req.body.password);
70
+ }
71
+ }
72
+ next();
73
+ };
74
+ const middlewareCheckAdmin = (req, res, next) => {
75
+ if (req.modelname !== "user")
76
+ return next();
77
+ const isAdmin = res.user?.admin;
78
+ if (!isAdmin) {
79
+ if (req.params)
80
+ req.params.admin = false;
81
+ if (req.body) {
82
+ for (const field of USER_PRIVILEGE_FIELDS) {
83
+ if (field in req.body) {
84
+ delete req.body[field];
85
+ }
86
+ }
87
+ }
88
+ }
89
+ next();
90
+ };
91
+ // Outputs whatever is in res.result as JSON
92
+ const outputJSON = async (req, res) => {
93
+ try {
94
+ res.send(res.result);
95
+ }
96
+ catch (err) {
97
+ console.error(new Date(), err);
98
+ throw new errors.InternalServerError(safeErrorMessage(err));
99
+ }
100
+ };
101
+ // Outputs whatever is in res.result as CSV
102
+ const outputCSV = (req, res, next) => {
103
+ const opts = { "flatten": true };
104
+ if (!res.result.data) {
105
+ throw new errors.InternalServerError("Error generating CSV");
106
+ }
107
+ try {
108
+ const data = res.result.data.map(row => row._doc);
109
+ if (!data.length) {
110
+ throw ("");
111
+ }
112
+ res.writeHead(200, {
113
+ 'Content-Type': 'text/csv',
114
+ 'Content-Disposition': 'attachment; filename=export.csv'
115
+ });
116
+ const csv = new CsvParser(opts).parse(data);
117
+ res.end(csv);
118
+ next();
119
+ }
120
+ catch (err) {
121
+ console.error(err);
122
+ throw new errors.InternalServerError(safeErrorMessage(err));
123
+ }
124
+ };
125
+ // Actions (verbs)
126
+ const actionGet = async (req, res) => {
127
+ const opname = `get ${req.modelname} ${ops++}`;
128
+ console.time(opname);
129
+ let filters = {};
130
+ try {
131
+ filters = parseFilter(req.query.filter);
132
+ filters = query_sanitize.sanitizeFilter(filters, getSecurityOpts(req));
133
+ }
134
+ catch (err) {
135
+ console.trace(new Date(), err);
136
+ // Preserve BadRequestError, convert others to InternalServerError
137
+ if (err instanceof errors.BadRequestError) {
138
+ throw err;
139
+ }
140
+ if (err instanceof errors.ForbiddenError) {
141
+ throw err;
142
+ }
143
+ throw new errors.InternalServerError(safeErrorMessage(err));
144
+ }
145
+ const search = query_sanitize.parseSearchObject(req.query.search);
146
+ for (const i in search) {
147
+ filters[i] = search[i];
148
+ }
149
+ let countquery = filters;
150
+ let qcount = req.Model.find(filters);
151
+ let q = req.Model.find(filters);
152
+ let checkDeleted = [{ _deleted: false }, { _deleted: null }];
153
+ if (!req.query.showDeleted) {
154
+ countquery = Object.assign({ $or: checkDeleted }, countquery);
155
+ qcount.or(checkDeleted);
156
+ q.or(checkDeleted);
157
+ }
158
+ if (req.query.search) {
159
+ // console.log({ search: req.query.search });
160
+ q = req.Model.find({ $text: { $search: req.query.search } }, { score: { $meta: "textScore" } }).sort({ score: { $meta: "textScore" } });
161
+ countquery = Object.assign({ $text: { $search: req.query.search } }, countquery);
162
+ qcount = req.Model.find({ $text: { $search: req.query.search } });
163
+ }
164
+ if (res.user) {
165
+ q.options = ({ user: res.user });
166
+ }
167
+ try {
168
+ const estimatedCount = await req.Model.estimatedDocumentCount();
169
+ let count = -1;
170
+ if (query_limits.shouldRunCount(req)) {
171
+ if (estimatedCount < 100000 && Object.keys(countquery).length !== 0) {
172
+ count = await qcount.countDocuments();
173
+ }
174
+ else {
175
+ count = estimatedCount;
176
+ }
177
+ }
178
+ const result = {};
179
+ if (count >= 0) {
180
+ result.count = count;
181
+ }
182
+ const effectiveLimit = query_limits.enforceListLimit(req, estimatedCount);
183
+ query_limits.applyListPagination(q, result, req, effectiveLimit, count >= 0 ? count : 0, changeUrlParams);
184
+ if (req.query.sort) {
185
+ q.sort(req.query.sort);
186
+ result.sort = req.query.sort;
187
+ }
188
+ if (req.query.populate) {
189
+ if ((typeof req.query.populate === "object") && !Array.isArray(req.query.populate)) {
190
+ for (let i in req.query.populate) {
191
+ q.populate(i, req.query.populate[i].replace(/,/g, " "));
192
+ }
193
+ }
194
+ else {
195
+ q.populate(req.query.populate);
196
+ }
197
+ result.populate = req.query.populate;
198
+ }
199
+ if (req.query.autopopulate) {
200
+ res.header("jxp-autopopulate-warning", "expensive");
201
+ for (let key in req.Model.schema.paths) {
202
+ const dirpath = req.Model.schema.paths[key];
203
+ if (dirpath.instance == "ObjectID" && dirpath.options.link) {
204
+ q.populate(String(dirpath.options.map_to || dirpath.options.virtual || dirpath.options.link.toLowerCase()));
205
+ }
206
+ }
207
+ result.autopopulate = true;
208
+ }
209
+ if (req.query.fields) {
210
+ const fields = req.query.fields.split(",");
211
+ const select = {};
212
+ fields.forEach(field => {
213
+ select[field] = 1;
214
+ });
215
+ q.select(select);
216
+ }
217
+ if (req.query.search) {
218
+ result.search = req.query.search;
219
+ }
220
+ result.data = await q.exec();
221
+ response_sanitize.sanitizeListResult(result, getStripFields(req));
222
+ res.result = result;
223
+ if (debug)
224
+ console.timeEnd(opname);
225
+ }
226
+ catch (err) {
227
+ console.error(new Date(), err);
228
+ if (debug)
229
+ console.timeEnd(opname);
230
+ // Preserve BadRequestError, convert others to InternalServerError
231
+ if (err instanceof errors.BadRequestError) {
232
+ throw err;
233
+ }
234
+ if (err instanceof errors.ForbiddenError) {
235
+ throw err;
236
+ }
237
+ if (err.code)
238
+ throw err;
239
+ throw new errors.InternalServerError(safeErrorMessage(err));
240
+ }
241
+ };
242
+ const actionGetOne = async (req, res) => {
243
+ const opname = `getOne ${req.modelname}/${req.params.item_id} ${ops++}`;
244
+ console.time(opname);
245
+ try {
246
+ const data = await getOne(req.Model, req.params.item_id, req.query, { user: res.user });
247
+ res.result = { data };
248
+ if (debug)
249
+ console.timeEnd(opname);
250
+ }
251
+ catch (err) {
252
+ console.error(new Date(), err);
253
+ if (debug)
254
+ console.timeEnd(opname);
255
+ if (err.code)
256
+ throw err;
257
+ throw new errors.InternalServerError(safeErrorMessage(err));
258
+ }
259
+ };
260
+ const actionPost = async (req, res) => {
261
+ const opname = `post ${req.modelname} ${ops++}`;
262
+ console.time(opname);
263
+ try {
264
+ let item = new req.Model();
265
+ _populateItem(item, datamunging.deserialize(req.body));
266
+ if (res.user) {
267
+ item._owner_id = res.user._id;
268
+ item.__user = res.user;
269
+ }
270
+ const result = await item.save();
271
+ let silence = req.params._silence;
272
+ if (req.body && req.body._silence)
273
+ silence = true;
274
+ if (!silence) {
275
+ req.config.callbacks.post.call(null, req.modelname, result, res.user);
276
+ ws.postHook.call(null, req.modelname, result, res.user);
277
+ }
278
+ res.json({
279
+ status: "ok",
280
+ message: req.modelname + " created",
281
+ data: item
282
+ });
283
+ if (debug)
284
+ console.timeEnd(opname);
285
+ }
286
+ catch (err) {
287
+ console.error(new Date(), err);
288
+ if (debug)
289
+ console.timeEnd(opname);
290
+ if (err.code)
291
+ throw err;
292
+ throw new errors.InternalServerError(safeErrorMessage(err));
293
+ }
294
+ };
295
+ const actionPut = async (req, res) => {
296
+ const opname = `put ${req.modelname}/${req.params.item_id} ${ops++}`;
297
+ console.time(opname);
298
+ try {
299
+ let item = await req.Model.findById(req.params.item_id);
300
+ if (!item) {
301
+ console.error(new Date(), "Document not found");
302
+ throw new errors.NotFoundError(`Document ${req.params.item_id} not found on ${req.modelname}`);
303
+ }
304
+ _populateItem(item, datamunging.deserialize(req.body));
305
+ _versionItem(item);
306
+ if (res.user) {
307
+ item.__user = res.user;
308
+ item._updated_by_id = res.user._id;
309
+ }
310
+ const data = await item.save();
311
+ let silence = req.params._silence;
312
+ if (req.body && req.body._silence)
313
+ silence = true;
314
+ if (!silence) {
315
+ req.config.callbacks.put.call(null, req.modelname, item, res.user);
316
+ ws.putHook.call(null, req.modelname, item, res.user);
317
+ }
318
+ res.json({
319
+ status: "ok",
320
+ message: req.modelname + " updated",
321
+ data: data
322
+ });
323
+ if (debug)
324
+ console.timeEnd(opname);
325
+ }
326
+ catch (err) {
327
+ console.error(new Date(), err);
328
+ if (debug)
329
+ console.timeEnd(opname);
330
+ if (err.code)
331
+ throw err;
332
+ throw new errors.InternalServerError(safeErrorMessage(err));
333
+ }
334
+ };
335
+ const actionUpdate = async (req, res) => {
336
+ const opname = `update ${req.modelname}/${req.params.item_id} ${ops++}`;
337
+ console.time(opname);
338
+ try {
339
+ const body_data = datamunging.deserialize(req.body);
340
+ const item = await req.Model.findById(req.params.item_id);
341
+ if (!item) {
342
+ throw new errors.NotFoundError(`Document ${req.params.item_id} not found on ${req.modelname}`);
343
+ }
344
+ _populateItem(item, body_data);
345
+ _versionItem(item);
346
+ if (res.user) {
347
+ item.__user = res.user;
348
+ item._updated_by_id = res.user._id;
349
+ }
350
+ const data = await item.save();
351
+ let silence = req.params._silence;
352
+ if (req.body && req.body._silence)
353
+ silence = true;
354
+ if (!silence) {
355
+ req.config.callbacks.put.call(null, req.modelname, item, res.user);
356
+ ws.putHook.call(null, req.modelname, item, res.user);
357
+ }
358
+ res.json({
359
+ status: "ok",
360
+ message: req.modelname + " updated",
361
+ data
362
+ });
363
+ if (debug)
364
+ console.timeEnd(opname);
365
+ }
366
+ catch (err) {
367
+ console.error(new Date(), err);
368
+ if (debug)
369
+ console.timeEnd(opname);
370
+ if (err.code)
371
+ throw err;
372
+ throw new errors.InternalServerError(safeErrorMessage(err));
373
+ }
374
+ };
375
+ const actionDelete = async (req, res) => {
376
+ const permaDelete = req.query._permaDelete;
377
+ const cascade = req.query._cascade;
378
+ let silence = req.query._silence || (req.body && req.body._silence);
379
+ const opname = `del ${req.modelname}/${req.params.item_id} ${ops++}`;
380
+ console.time(opname);
381
+ try {
382
+ let item = await req.Model.findById(req.params.item_id);
383
+ if (!item) {
384
+ throw new errors.NotFoundError(`Couldn't find item ${req.params.item_id} for delete on ${req.modelname}`);
385
+ }
386
+ const linked_models = link_index.getReferrers(req.Model.modelName);
387
+ const referrerChecks = linked_models.map(async (linked_model) => {
388
+ const q = {};
389
+ q[linked_model.field] = item._id;
390
+ const check = await models[linked_model.modelname].countDocuments(q);
391
+ if (check) {
392
+ if (cascade) {
393
+ if (permaDelete) {
394
+ await models[linked_model.modelname].deleteMany(q);
395
+ }
396
+ else {
397
+ await models[linked_model.modelname].updateMany(q, { _deleted: true });
398
+ }
399
+ }
400
+ else {
401
+ throw new errors.ConflictError(`Parent link item exists in ${linked_model.modelname}/${linked_model.field}`);
402
+ }
403
+ }
404
+ });
405
+ await Promise.all(referrerChecks);
406
+ if (res.user) {
407
+ item.__user = res.user;
408
+ }
409
+ if (Object.prototype.hasOwnProperty.call(req.Model.schema.paths, "_deleted") && !(permaDelete)) {
410
+ item._deleted = true;
411
+ _versionItem(item);
412
+ await item.save();
413
+ }
414
+ else {
415
+ // console.log("Hard deleting");
416
+ await req.Model.deleteOne({ _id: item._id });
417
+ }
418
+ if (!silence) {
419
+ req.config.callbacks.delete.call(null, req.modelname, item, res.user, { soft: false });
420
+ ws.delHook.call(null, req.modelname, item, res.user);
421
+ }
422
+ res.json({
423
+ status: "ok",
424
+ message: `${req.modelname}/${req.params.item_id} deleted`
425
+ });
426
+ if (debug)
427
+ console.timeEnd(opname);
428
+ }
429
+ catch (err) {
430
+ console.error(new Date(), err);
431
+ if (debug)
432
+ console.timeEnd(opname);
433
+ if (err.code)
434
+ throw err;
435
+ throw new errors.InternalServerError(safeErrorMessage(err));
436
+ }
437
+ };
438
+ const actionCount = async (req, res) => {
439
+ const opname = `count ${req.modelname} ${ops++}`;
440
+ console.time(opname);
441
+ let filters = {};
442
+ try {
443
+ filters = parseFilter(req.query.filter);
444
+ filters = query_sanitize.sanitizeFilter(filters, getSecurityOpts(req));
445
+ }
446
+ catch (err) {
447
+ console.trace(new Date(), err);
448
+ if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
449
+ throw err;
450
+ }
451
+ throw new errors.InternalServerError(safeErrorMessage(err));
452
+ }
453
+ const search = query_sanitize.parseSearchObject(req.query.search);
454
+ for (const i in search) {
455
+ filters[i] = search[i];
456
+ }
457
+ if (!req.query.showDeleted) {
458
+ filters = Object.assign({ $or: [{ _deleted: false }, { _deleted: null }] }, filters);
459
+ }
460
+ try {
461
+ const count = await req.Model.countDocuments(filters).exec();
462
+ res.result = { count };
463
+ if (debug)
464
+ console.timeEnd(opname);
465
+ }
466
+ catch (err) {
467
+ console.error(new Date(), err);
468
+ if (debug)
469
+ console.timeEnd(opname);
470
+ if (err.code)
471
+ throw err;
472
+ throw new errors.InternalServerError(safeErrorMessage(err));
473
+ }
474
+ };
475
+ const actionCall = async (req, res) => {
476
+ req.body = req.body || {};
477
+ req.body.__user = res.user || null;
478
+ try {
479
+ call_guard.assertCallableStatic(req.Model, req.params.method_name);
480
+ const result = await req.Model[req.params.method_name](req.body);
481
+ res.json(result);
482
+ }
483
+ catch (err) {
484
+ console.error(new Date(), err);
485
+ if (err.code)
486
+ throw err;
487
+ if (err instanceof errors.ForbiddenError || err instanceof errors.NotFoundError) {
488
+ throw err;
489
+ }
490
+ throw new errors.InternalServerError(safeErrorMessage(err));
491
+ }
492
+ };
493
+ const actionCallItem = async (req, res) => {
494
+ try {
495
+ call_guard.assertCallableStatic(req.Model, req.params.method_name);
496
+ const item = await req.Model.findById(req.params.item_id);
497
+ if (!item) {
498
+ throw new errors.NotFoundError(`Couldn't find item ${req.params.item_id} on ${req.modelname} for call`);
499
+ }
500
+ if (item._deleted && !req.query.showDeleted) {
501
+ throw new errors.NotFoundError(`Document ${req.params.item_id} is deleted on ${req.modelname}`);
502
+ }
503
+ const body = req.body || {};
504
+ body.__user = res.user || null;
505
+ const result = await req.Model[req.params.method_name](item, body);
506
+ res.json(result);
507
+ }
508
+ catch (err) {
509
+ console.trace(err);
510
+ if (err.code)
511
+ throw err;
512
+ if (err instanceof errors.ForbiddenError || err instanceof errors.NotFoundError) {
513
+ throw err;
514
+ }
515
+ throw new errors.InternalServerError(safeErrorMessage(err));
516
+ }
517
+ };
518
+ // Actions (verbs)
519
+ const actionQuery = async (req, res) => {
520
+ if (!advancedQueryAllowed(req.Model, "query")) {
521
+ throw new errors.ForbiddenError(`POST /query is disabled for model ${req.modelname}`);
522
+ }
523
+ if (!req.body || !req.body.query || typeof req.body.query !== "object") {
524
+ throw new errors.BadRequestError("Query missing or not of type object");
525
+ }
526
+ const opname = `query ${req.modelname} ${ops++}`;
527
+ console.time(opname);
528
+ const sanitizedQuery = query_sanitize.sanitizeFilter(req.body.query, getSecurityOpts(req));
529
+ let query = [sanitizedQuery];
530
+ let checkDeleted = { "$or": [{ _deleted: false }, { _deleted: null }] };
531
+ if (!req.query.showDeleted) {
532
+ query.push(checkDeleted);
533
+ }
534
+ let qcount = req.Model.find({ "$and": query });
535
+ let q = req.Model.find({ "$and": query });
536
+ try {
537
+ let count = -1;
538
+ if (query_limits.shouldRunCount(req)) {
539
+ count = await qcount.countDocuments();
540
+ }
541
+ const result = {};
542
+ if (count >= 0) {
543
+ result.count = count;
544
+ }
545
+ const estimatedCount = await req.Model.estimatedDocumentCount();
546
+ const effectiveLimit = query_limits.enforceListLimit(req, estimatedCount);
547
+ query_limits.applyListPagination(q, result, req, effectiveLimit, count >= 0 ? count : 0, changeUrlParams);
548
+ if (req.query.sort) {
549
+ q.sort(req.query.sort);
550
+ result.sort = req.query.sort;
551
+ }
552
+ if (req.query.populate) {
553
+ if ((typeof req.query.populate === "object") && !Array.isArray(req.query.populate)) {
554
+ for (let i in req.query.populate) {
555
+ q.populate(i, req.query.populate[i].replace(/,/g, " "));
556
+ }
557
+ }
558
+ else {
559
+ q.populate(req.query.populate);
560
+ }
561
+ result.populate = req.query.populate;
562
+ }
563
+ if (req.query.autopopulate) {
564
+ for (let key in req.Model.schema.paths) {
565
+ const dirpath = req.Model.schema.paths[key];
566
+ if (dirpath.instance == "ObjectID" && dirpath.options.link) {
567
+ q.populate(String(dirpath.options.map_to || dirpath.options.virtual || dirpath.options.link));
568
+ }
569
+ }
570
+ result.autopopulate = true;
571
+ }
572
+ if (req.query.fields) {
573
+ const fields = req.query.fields.split(",");
574
+ const select = {};
575
+ fields.forEach(field => {
576
+ select[field] = 1;
577
+ });
578
+ q.select(select);
579
+ }
580
+ result.data = await q.exec();
581
+ response_sanitize.sanitizeListResult(result, getStripFields(req));
582
+ res.result = result;
583
+ if (debug)
584
+ console.timeEnd(opname);
585
+ res.json(result);
586
+ }
587
+ catch (err) {
588
+ console.error(new Date(), err);
589
+ if (debug)
590
+ console.timeEnd(opname);
591
+ if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
592
+ throw err;
593
+ }
594
+ if (err.code)
595
+ throw err;
596
+ throw new errors.InternalServerError(safeErrorMessage(err));
597
+ }
598
+ };
599
+ // Actions (verbs)
600
+ const actionAggregate = async (req, res) => {
601
+ if (!advancedQueryAllowed(req.Model, "aggregate")) {
602
+ throw new errors.ForbiddenError(`POST /aggregate is disabled for model ${req.modelname}`);
603
+ }
604
+ let query = (req.body.query) ? req.body.query : req.body; // Don't require to embed in query anymore
605
+ if (!query || !Array.isArray(query)) {
606
+ console.error("query missing or not of type array");
607
+ throw new errors.BadRequestError("Query missing or not of type array");
608
+ }
609
+ query = query_manipulation.fix_query(query);
610
+ aggregate_guard.validatePipeline(query, {
611
+ aggregate_stages_allow: getSecurityOpts(req).aggregate_stages_allow,
612
+ isAdmin: res.user?.admin,
613
+ });
614
+ const opname = `aggregate ${req.modelname} ${ops++}`;
615
+ console.time(opname);
616
+ try {
617
+ let result = {};
618
+ if (req.query.allowDiskUse) {
619
+ result.data = await req.Model.aggregate(query).allowDiskUse(true).exec();
620
+ }
621
+ else {
622
+ result.data = await req.Model.aggregate(query);
623
+ }
624
+ response_sanitize.sanitizeResponse(result, getStripFields(req));
625
+ res.result = result;
626
+ if (debug)
627
+ console.timeEnd(opname);
628
+ res.json(result);
629
+ }
630
+ catch (err) {
631
+ console.error(new Date(), err);
632
+ if (debug)
633
+ console.timeEnd(opname);
634
+ if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
635
+ throw err;
636
+ }
637
+ throw new errors.InternalServerError(safeErrorMessage(err));
638
+ }
639
+ };
640
+ // Actions (verbs)
641
+ const actionBulkWrite = async (req, res) => {
642
+ if (!advancedQueryAllowed(req.Model, "bulkwrite")) {
643
+ throw new errors.ForbiddenError(`POST /bulkwrite is disabled for model ${req.modelname}`);
644
+ }
645
+ if (!req.body || !Array.isArray(req.body)) {
646
+ console.error("query missing or not of type array");
647
+ throw new errors.BadRequestError("Query missing or not of type array");
648
+ }
649
+ const opname = `bulkwrite ${req.modelname} ${ops++}`;
650
+ console.time(opname);
651
+ const query = req.body;
652
+ bulkwrite_guard.validateBulkOps(query, {
653
+ bulk_operations_allow: getSecurityOpts(req).bulk_operations_allow,
654
+ isAdmin: res.user?.admin,
655
+ });
656
+ try {
657
+ let result = {};
658
+ result.data = await req.Model.bulkWrite(query);
659
+ res.result = result;
660
+ if (debug)
661
+ console.timeEnd(opname);
662
+ res.json(result);
663
+ }
664
+ catch (err) {
665
+ console.error(new Date(), err);
666
+ if (debug)
667
+ console.timeEnd(opname);
668
+ if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
669
+ throw err;
670
+ }
671
+ throw new errors.InternalServerError(safeErrorMessage(err));
672
+ }
673
+ };
674
+ // var actionBatch = (req, res, next) => {
675
+ // console.time("BATCH " + req.modelname);
676
+ // var items = [];
677
+ // data = JSON.parse(req.params.json);
678
+ // data.forEach(function(data) {
679
+ // var item = new req.Model();
680
+ // if (res.user) {
681
+ // item.__user = res.user;
682
+ // }
683
+ // _populateItem(item, data);
684
+ // _versionItem(item);
685
+ // if (res.user) {
686
+ // item._owner_id = res.user._id;
687
+ // }
688
+ // items.push(item);
689
+ // });
690
+ // req.Model.create(items, function(err, docs) {
691
+ // if (err) {
692
+ // console.error(err);
693
+ // res.status(500).send(err.toString());
694
+ // } else {
695
+ // // websocket.emit(modelname, { method: "post", _id: result._id });
696
+ // console.log({ action_id: 8, action: "Batch insert", type: req.modelname, count: items.length, user: filterLogUser(res.user) });
697
+ // res.send({ message: req.modelname + " created ", data: items.length });
698
+ // if (debug) console.timeEnd("BATCH " + req.modelname);
699
+ // return;
700
+ // }
701
+ // });
702
+ // };
703
+ // Utitlities
704
+ const getOne = async (Model, item_id, params, options) => {
705
+ const query = Model.findById(item_id, {}, options);
706
+ if (params.populate) {
707
+ if ((typeof params.populate === "object") && !Array.isArray(params.populate)) {
708
+ for (let i in params.populate) {
709
+ query.populate(i, params.populate[i].replace(/,/g, " "));
710
+ }
711
+ }
712
+ else {
713
+ query.populate(params.populate);
714
+ }
715
+ }
716
+ if (params.autopopulate) {
717
+ for (let key in Model.schema.paths) {
718
+ var dirpath = Model.schema.paths[key];
719
+ if (dirpath.instance == "ObjectID" && dirpath.options.link) {
720
+ query.populate(String(dirpath.options.map_to || dirpath.options.virtual || dirpath.options.link.toLowerCase()));
721
+ }
722
+ }
723
+ }
724
+ try {
725
+ var item = await query.exec();
726
+ if (!item) {
727
+ // console.error("Could not find document");
728
+ throw new errors.NotFoundError(`Could not find document ${item_id} on ${Model.modelName}`);
729
+ }
730
+ if (item._deleted && !params.showDeleted) {
731
+ // console.error("Document is deleted");
732
+ throw new errors.NotFoundError(`Document ${item_id} is deleted on ${Model.modelName}`);
733
+ }
734
+ return response_sanitize.sanitizeDocument(item);
735
+ }
736
+ catch (err) {
737
+ console.error(err);
738
+ if (err.code)
739
+ throw err;
740
+ throw new errors.InternalServerError(safeErrorMessage(err));
741
+ }
742
+ };
743
+ // Helper function to check if a string is an ISO date string
744
+ function isISODateString(str) {
745
+ if (typeof str !== 'string')
746
+ return false;
747
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
748
+ if (!isoDateRegex.test(str))
749
+ return false;
750
+ const date = new Date(str);
751
+ if (isNaN(date.getTime())) {
752
+ throw new errors.BadRequestError("Invalid date format");
753
+ }
754
+ return true;
755
+ }
756
+ const parseFilter = (filter, depth = 0) => {
757
+ const MAX_DEPTH = 10;
758
+ if (!filter)
759
+ return {};
760
+ if (depth > MAX_DEPTH) {
761
+ throw new errors.BadRequestError("Maximum filter depth exceeded");
762
+ }
763
+ if (typeof filter !== "object" || filter === null)
764
+ return filter;
765
+ // Handle arrays by merging their operators
766
+ if (Array.isArray(filter)) {
767
+ const result = {};
768
+ filter.forEach(item => {
769
+ if (typeof item === "string" && item.includes(":")) {
770
+ const parts = item.split(":");
771
+ const key = parts[0];
772
+ const value = parts.slice(1).join(":");
773
+ if (key.startsWith("$")) {
774
+ try {
775
+ if (isISODateString(value)) {
776
+ result[key] = new Date(value);
777
+ }
778
+ else {
779
+ result[key] = value;
780
+ }
781
+ }
782
+ catch (err) {
783
+ if (err instanceof errors.BadRequestError) {
784
+ throw err;
785
+ }
786
+ throw new errors.BadRequestError("Invalid date format");
787
+ }
788
+ }
789
+ }
790
+ });
791
+ return result;
792
+ }
793
+ // Create a new object to avoid modifying the input
794
+ const parsedFilter = {};
795
+ for (let i in filter) {
796
+ if (filter[i] === "false") {
797
+ parsedFilter[i] = false;
798
+ continue;
799
+ }
800
+ if (filter[i] === "true") {
801
+ parsedFilter[i] = true;
802
+ continue;
803
+ }
804
+ if (typeof filter[i] === "string") {
805
+ try {
806
+ if (isISODateString(filter[i])) {
807
+ parsedFilter[i] = new Date(filter[i]);
808
+ continue;
809
+ }
810
+ }
811
+ catch (err) {
812
+ if (err instanceof errors.BadRequestError) {
813
+ throw err;
814
+ }
815
+ throw new errors.BadRequestError("Invalid date format");
816
+ }
817
+ if (filter[i].includes(":")) {
818
+ const parts = filter[i].split(":");
819
+ const key = parts[0];
820
+ const value = parts.slice(1).join(":");
821
+ if (key.startsWith("$")) {
822
+ try {
823
+ if (isISODateString(value)) {
824
+ if (!parsedFilter[i])
825
+ parsedFilter[i] = {};
826
+ parsedFilter[i][key] = new Date(value);
827
+ }
828
+ else if (value.startsWith("[") && value.endsWith("]")) {
829
+ if (!parsedFilter[i])
830
+ parsedFilter[i] = {};
831
+ parsedFilter[i][key] = value.slice(1, -1).split(",");
832
+ }
833
+ else if (key === "$regex" && value.startsWith("/")) {
834
+ if (!parsedFilter[i])
835
+ parsedFilter[i] = {};
836
+ const match = value.match(/^\/(.+?)\/([gimy]*)$/);
837
+ if (match) {
838
+ parsedFilter[i][key] = new RegExp(match[1], match[2]);
839
+ }
840
+ }
841
+ else {
842
+ if (!parsedFilter[i])
843
+ parsedFilter[i] = {};
844
+ parsedFilter[i][key] = value;
845
+ }
846
+ }
847
+ catch (err) {
848
+ if (err instanceof errors.BadRequestError) {
849
+ throw err;
850
+ }
851
+ throw new errors.BadRequestError("Invalid date format");
852
+ }
853
+ }
854
+ else {
855
+ parsedFilter[i] = filter[i];
856
+ }
857
+ }
858
+ else {
859
+ parsedFilter[i] = filter[i];
860
+ }
861
+ }
862
+ else if (Array.isArray(filter[i])) {
863
+ parsedFilter[i] = parseFilter(filter[i], depth + 1);
864
+ }
865
+ else if (typeof filter[i] === "object") {
866
+ parsedFilter[i] = parseFilter(filter[i], depth + 1);
867
+ }
868
+ else {
869
+ parsedFilter[i] = filter[i];
870
+ }
871
+ }
872
+ return parsedFilter;
873
+ };
874
+ const _deSerialize = (data) => {
875
+ function assign(obj, keyPath, value) {
876
+ const MAX_DEPTH = 20; // Prevent excessive nesting
877
+ const lastKeyIndex = keyPath.length - 1;
878
+ if (lastKeyIndex >= MAX_DEPTH) {
879
+ console.warn('Maximum nesting depth exceeded in _deSerialize');
880
+ return;
881
+ }
882
+ for (let i = 0; i < lastKeyIndex; ++i) {
883
+ let key = keyPath[i];
884
+ if (!(key in obj))
885
+ obj[key] = {};
886
+ obj = obj[key];
887
+ }
888
+ obj[keyPath[lastKeyIndex]] = value;
889
+ }
890
+ if (!data || typeof data !== 'object')
891
+ return;
892
+ for (let datum in data) {
893
+ const matches = datum.match(/\[(.+?)\]/g);
894
+ if (matches) {
895
+ const params = matches.map(function (match) {
896
+ return match.replace(/[[\]]/g, "");
897
+ });
898
+ if (isNaN(Number(params[0]))) {
899
+ params.unshift(datum.match(/(.+?)\[/)[1]);
900
+ assign(data, params, data[datum]);
901
+ }
902
+ }
903
+ }
904
+ };
905
+ const _populateItem = (item, data) => {
906
+ _deSerialize(data);
907
+ for (let prop in item) {
908
+ if (typeof data[prop] != "undefined") {
909
+ item[prop] = data[prop];
910
+ // Unset any blank values - essentially 'deleting' values on editing
911
+ if (data[prop] === "") {
912
+ item[prop] = null;
913
+ }
914
+ }
915
+ //Check for arrays that come in like param[1]=blah, param[2]=yack
916
+ if (data[prop + "[0]"]) {
917
+ var x = 0;
918
+ var tmp = [];
919
+ while (data[prop + "[" + x + "]"]) {
920
+ tmp.push(data[prop + "[" + x + "]"]);
921
+ x++;
922
+ }
923
+ item[prop] = tmp;
924
+ }
925
+ }
926
+ };
927
+ const _versionItem = (item) => {
928
+ if (item._version || item._version === 0) {
929
+ item._version++;
930
+ }
931
+ else {
932
+ item._version = 0;
933
+ }
934
+ };
935
+ const _fixArrays = (req, res, next) => {
936
+ if (req.body) {
937
+ for (var i in req.body) {
938
+ if (i.search(/\[\d+\]/) > -1) {
939
+ var parts = i.match(/(^[A-Za-z]+)(\[)/);
940
+ var el = parts[1];
941
+ if (!req.body[el]) {
942
+ req.body[el] = [];
943
+ }
944
+ req.body[el].push(req.body[i]);
945
+ }
946
+ }
947
+ }
948
+ next();
949
+ };
950
+ const changeUrlParams = (req, key, val) => {
951
+ var q = req.query;
952
+ q[key] = val;
953
+ return req.config.url + req.path() + "?" + querystring.stringify(q);
954
+ };
955
+ const JXP = function (options) {
956
+ const server = restify.createServer();
957
+ const model_dir = options.model_dir || modeldir.findModelDir(path.dirname(process.argv[1]));
958
+ //Set up config with default
959
+ var config = {
960
+ model_dir: path.join(model_dir),
961
+ mongo: options.mongo,
962
+ callbacks: {
963
+ put: function () { },
964
+ post: function () { },
965
+ delete: function () { },
966
+ get: function () { },
967
+ getOne: function () { },
968
+ update: function () { },
969
+ },
970
+ log: "access.log",
971
+ pre_hooks: {
972
+ login: (req, res, next) => {
973
+ next();
974
+ },
975
+ get: (req, res, next) => {
976
+ next();
977
+ },
978
+ getOne: (req, res, next) => {
979
+ next();
980
+ },
981
+ post: (req, res, next) => {
982
+ next();
983
+ },
984
+ put: (req, res, next) => {
985
+ next();
986
+ },
987
+ update: (req, res, next) => {
988
+ next();
989
+ },
990
+ delete: (req, res, next) => {
991
+ next();
992
+ }
993
+ },
994
+ post_hooks: {
995
+ login: async () => {
996
+ },
997
+ },
998
+ cache_timeout: "5 minutes",
999
+ query_limits: {
1000
+ enabled: true,
1001
+ large_collection_threshold: 10000,
1002
+ max: 1000,
1003
+ default: 100,
1004
+ require_limit_always: true,
1005
+ skip_count_unless_paginated: true,
1006
+ },
1007
+ security: {
1008
+ strip_fields: ["password"],
1009
+ },
1010
+ cors: {
1011
+ origins: ["*"],
1012
+ },
1013
+ };
1014
+ //Override config with passed in options
1015
+ for (let i in options) {
1016
+ if (typeof config[i] === "object" && !Array.isArray(config[i])) {
1017
+ if (typeof options[i] === "object" && !Array.isArray(options[i])) {
1018
+ for (let j in options[i]) {
1019
+ config[i][j] = options[i][j]; // Second level object copy
1020
+ }
1021
+ }
1022
+ }
1023
+ else {
1024
+ config[i] = options[i];
1025
+ }
1026
+ if (i === "model_dir" || i === "log") {
1027
+ const value = String(options[i]);
1028
+ // Absolute paths unchanged; relative paths resolve from project cwd (npm scripts)
1029
+ config[i] = value.charAt(0) === "/" ? value : path.resolve(process.cwd(), value);
1030
+ }
1031
+ }
1032
+ if (config.debug)
1033
+ debug = true;
1034
+ // Set apikey and server globally to inject into schemas
1035
+ global.apikey = config.apikey;
1036
+ global.server = config.server;
1037
+ global.model_dir = config.model_dir;
1038
+ // Pre-load models
1039
+ var files = fs.readdirSync(config.model_dir);
1040
+ let modelnames = files.filter(function (fname) {
1041
+ return fname.endsWith("_model.js");
1042
+ });
1043
+ modelnames.forEach(function (fname) {
1044
+ var modelname = fname.replace("_model.js", "");
1045
+ const mod = require(path.join(config.model_dir, fname));
1046
+ models[modelname] = mod.default || mod;
1047
+ });
1048
+ link_index.buildLinkIndex(models);
1049
+ setup.init(config);
1050
+ security.init(config);
1051
+ login.init(config);
1052
+ groups.init(config);
1053
+ ws.init({ models });
1054
+ cache.init(config);
1055
+ const docs = new Docs({ config, models });
1056
+ // Set up our API server
1057
+ // Rate limitting
1058
+ if (config.throttle) {
1059
+ server.use(restify.plugins.throttle(config.throttle));
1060
+ }
1061
+ // Logging
1062
+ if (!config.quiet_startup) {
1063
+ console.log("Logging to", config.log);
1064
+ }
1065
+ var accessLogStream = fs.createWriteStream(config.log, { flags: "a" });
1066
+ server.use(morgan("combined", { stream: accessLogStream }));
1067
+ // CORS
1068
+ const corsOrigins = config.cors?.origins?.length ? config.cors.origins : ["*"];
1069
+ const cors = corsMiddleware({
1070
+ preflightMaxAge: 5, //Optional
1071
+ origins: corsOrigins,
1072
+ allowHeaders: ['X-Requested-With', 'Authorization'],
1073
+ exposeHeaders: ['Authorization']
1074
+ });
1075
+ server.pre(cors.preflight);
1076
+ server.use(cors.actual);
1077
+ // Parse data
1078
+ server.use(restify.plugins.queryParser());
1079
+ server.use(restify.plugins.bodyParser());
1080
+ // Bind our config to req.config
1081
+ server.use((req, res, next) => {
1082
+ req.config = config;
1083
+ next();
1084
+ });
1085
+ // Set req.username = "anonymous" if not logged in
1086
+ server.use((req, res, next) => {
1087
+ if (!req.username)
1088
+ req.username = "anonymous";
1089
+ next();
1090
+ });
1091
+ // Define our endpoints
1092
+ /* Our API endpoints */
1093
+ server.get("/api/:modelname", middlewareModel, security.login, security.auth, config.pre_hooks.get, cache.get, actionGet, cache.set, outputJSON);
1094
+ server.get("/api/:modelname/:item_id", middlewareModel, security.login, security.auth, config.pre_hooks.getOne, cache.get, actionGetOne, cache.set, outputJSON);
1095
+ server.post("/api/:modelname", middlewareModel, security.login, security.auth, middlewarePasswords, config.pre_hooks.post, actionPost, cache.clearAll, (req, res, next) => {
1096
+ next();
1097
+ });
1098
+ server.put("/api/:modelname/:item_id", middlewareModel, security.login, security.auth, middlewarePasswords, middlewareCheckAdmin, config.pre_hooks.put, actionPut, cache.clearAll, (req, res, next) => {
1099
+ next();
1100
+ });
1101
+ server.del("/api/:modelname/:item_id", middlewareModel, security.login, security.auth, config.pre_hooks.delete, actionDelete, cache.clearAll);
1102
+ // Count
1103
+ server.get("/count/:modelname", middlewareModel, security.login, security.auth, config.pre_hooks.get,
1104
+ // cache.get,
1105
+ actionCount,
1106
+ // cache.set,
1107
+ outputJSON);
1108
+ // CSV endpoints
1109
+ server.get("/csv/:modelname", middlewareModel, security.login, security.auth, config.pre_hooks.get, actionGet, outputCSV);
1110
+ // Query endpoints
1111
+ server.post("/query/:modelname", middlewareModel, security.login, security.auth, config.pre_hooks.get, actionQuery);
1112
+ server.post("/aggregate/:modelname", middlewareModel, security.login, security.auth, config.pre_hooks.get, actionAggregate);
1113
+ server.post("/bulkwrite/:modelname", middlewareModel, security.login, security.bulkAuth, config.pre_hooks.get, actionBulkWrite);
1114
+ server.post("/update/:modelname/:item_id", middlewareModel, security.login, security.auth, middlewarePasswords, middlewareCheckAdmin, config.pre_hooks.update, actionUpdate, cache.clearAll);
1115
+ /* Batch routes - ROLLED BACK FOR NOW */
1116
+ // server.post('/batch/create/:modelname', middlewareModel, security.login, security.auth, actionBatch);
1117
+ /* Call Methods in our models */
1118
+ server.get("/call/:modelname/:method_name", middlewareModel, security.login, security.auth, actionCall, cache.clearAll);
1119
+ server.post("/call/:modelname/:method_name", middlewareModel, security.login, security.auth, actionCall, cache.clearAll);
1120
+ server.get("/call/:modelname/:item_id/:method_name", middlewareModel, security.login, security.auth, actionCallItem, cache.clearAll);
1121
+ /* Login and authentication */
1122
+ server.post("/login/recover", login.recover);
1123
+ server.post("/login/getjwt", security.login, login.getJWT);
1124
+ server.get("/login/logout", security.login, login.logout);
1125
+ server.get("/logout", security.login, login.logout);
1126
+ server.get("/login/oauth/:provider", login.oauth);
1127
+ server.get("/login/oauth/callback/:provider", login.oauth_callback);
1128
+ server.post("/login", config.pre_hooks.login, login.login, config.post_hooks.login, outputJSON);
1129
+ server.post("/refresh", security.refresh);
1130
+ server.post("/login/refresh", security.refresh);
1131
+ /* Groups */
1132
+ server.put("/groups/:user_id", security.login, security.admin_only, _fixArrays, groups.actionPut);
1133
+ server.post("/groups/:user_id", security.login, security.admin_only, _fixArrays, groups.actionPost);
1134
+ server.get("/groups/:user_id", security.login, groups.actionGet);
1135
+ server.del("/groups/:user_id", security.login, security.admin_only, groups.actionDelete);
1136
+ /* Meta */
1137
+ server.get("/model/:modelname", middlewareModel, docs.metaModel.bind(docs));
1138
+ server.get("/model", docs.metaModels.bind(docs));
1139
+ // server.get("/docs/_design", docs.dbDiagram.bind(docs));
1140
+ server.get("/docs/assets/:file", docs.serveAsset.bind(docs));
1141
+ server.get("/docs/api", docs.apiIndex.bind(docs));
1142
+ server.get("/docs/md/:md_doc", docs.md.bind(docs));
1143
+ server.get("/docs/model/:modelname", docs.model.bind(docs));
1144
+ server.get("/", docs.frontPage.bind(docs));
1145
+ /* Setup */
1146
+ server.get("/setup", setup.checkUserDoesNotExist, setup.setup);
1147
+ server.post("/setup", setup.checkUserDoesNotExist, setup.setup);
1148
+ server.post("/setup/data", setup.checkUserDoesNotExist, setup.data_setup);
1149
+ /* Websocket */
1150
+ server.on("upgrade", ws.upgrade);
1151
+ /* Cache */
1152
+ server.get("/cache/stats", security.login, security.admin_only, cache.stats, outputJSON);
1153
+ server.get("/cache/clear", security.login, security.admin_only, cache.clearAll, outputJSON);
1154
+ return server;
1155
+ };
1156
+ module.exports = JXP;
1157
+ //# sourceMappingURL=jxp.js.map