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