jxp 4.0.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.env.sample +12 -0
  2. package/dist/libs/bulkwrite_guard.d.ts +1 -0
  3. package/dist/libs/bulkwrite_guard.d.ts.map +1 -1
  4. package/dist/libs/bulkwrite_guard.js +43 -0
  5. package/dist/libs/bulkwrite_guard.js.map +1 -1
  6. package/dist/libs/docs-auth.d.ts +25 -0
  7. package/dist/libs/docs-auth.d.ts.map +1 -0
  8. package/dist/libs/docs-auth.js +205 -0
  9. package/dist/libs/docs-auth.js.map +1 -0
  10. package/dist/libs/docs.js +26 -5
  11. package/dist/libs/docs.js.map +1 -1
  12. package/dist/libs/jxp.d.ts.map +1 -1
  13. package/dist/libs/jxp.js +118 -53
  14. package/dist/libs/jxp.js.map +1 -1
  15. package/dist/libs/load-config.d.ts.map +1 -1
  16. package/dist/libs/load-config.js +11 -0
  17. package/dist/libs/load-config.js.map +1 -1
  18. package/dist/libs/login_rate_limit.d.ts +16 -0
  19. package/dist/libs/login_rate_limit.d.ts.map +1 -0
  20. package/dist/libs/login_rate_limit.js +49 -0
  21. package/dist/libs/login_rate_limit.js.map +1 -0
  22. package/dist/libs/query_limits.js +9 -3
  23. package/dist/libs/query_limits.js.map +1 -1
  24. package/dist/libs/request_log.d.ts +44 -0
  25. package/dist/libs/request_log.d.ts.map +1 -0
  26. package/dist/libs/request_log.js +229 -0
  27. package/dist/libs/request_log.js.map +1 -0
  28. package/dist/libs/security.js +18 -9
  29. package/dist/libs/security.js.map +1 -1
  30. package/dist/models/test_model.js +1 -1
  31. package/dist/models/test_model.js.map +1 -1
  32. package/dist/types/jxp-config.d.ts +17 -0
  33. package/dist/types/jxp-config.d.ts.map +1 -1
  34. package/docs/bulk_writes.md +9 -2
  35. package/docs/changelog.md +76 -1
  36. package/docs/configuration.md +30 -0
  37. package/package.json +2 -2
  38. package/templates/assets/api-console.js +19 -1
  39. package/templates/assets/docs-login.js +66 -0
  40. package/templates/assets/docs.css +17 -0
  41. package/templates/index.pug +9 -4
  42. package/templates/layout.pug +8 -4
  43. package/templates/login.pug +39 -0
  44. package/templates/navbar.pug +18 -7
package/dist/libs/jxp.js CHANGED
@@ -8,6 +8,8 @@ const login = require("./login");
8
8
  const groups = require("./groups");
9
9
  const setup = require("./setup");
10
10
  const Docs = require("./docs");
11
+ const docsAuth = require("./docs-auth");
12
+ const loginRateLimit = require("./login_rate_limit");
11
13
  const querystring = require("node:querystring");
12
14
  const fs = require("fs");
13
15
  const morgan = require("morgan");
@@ -25,6 +27,7 @@ const call_guard = require("./call_guard");
25
27
  const response_sanitize = require("./response_sanitize");
26
28
  const link_index = require("./link_index");
27
29
  const { safeErrorMessage } = require("./safe_error");
30
+ const { logRequestError, logAndThrow } = require("./request_log");
28
31
  const schemaModule = require("./schema");
29
32
  global.JXPSchema = schemaModule.default || schemaModule;
30
33
  var models = {};
@@ -37,10 +40,12 @@ function getStripFields(req) {
37
40
  function getSecurityOpts(req) {
38
41
  return req.config?.security || {};
39
42
  }
40
- function advancedQueryAllowed(Model, kind) {
43
+ function advancedQueryAllowed(Model, kind, options) {
41
44
  const opts = Model.schema.opts;
42
45
  const aq = opts?.advanced_queries;
43
46
  if (kind === "bulkwrite") {
47
+ if (options?.isAdmin)
48
+ return true;
44
49
  return aq?.bulkwrite === true;
45
50
  }
46
51
  if (kind === "aggregate") {
@@ -48,13 +53,33 @@ function advancedQueryAllowed(Model, kind) {
48
53
  }
49
54
  return aq?.query !== false;
50
55
  }
56
+ const middlewareBulkWriteAllowed = (req, res, next) => {
57
+ if (!advancedQueryAllowed(req.Model, "bulkwrite", { isAdmin: !!res.user?.admin })) {
58
+ const err = new errors.ForbiddenError(`POST /bulkwrite is disabled for model ${req.modelname}`);
59
+ logRequestError(req, res, err, "bulkwrite disabled");
60
+ return next(err);
61
+ }
62
+ next();
63
+ };
64
+ const middlewareAdvancedQueryAllowed = (kind) => {
65
+ return (req, res, next) => {
66
+ if (!advancedQueryAllowed(req.Model, kind)) {
67
+ const err = new errors.ForbiddenError(`POST /${kind} is disabled for model ${req.modelname}`);
68
+ logRequestError(req, res, err, `${kind} disabled`);
69
+ return next(err);
70
+ }
71
+ next();
72
+ };
73
+ };
51
74
  // Middleware
52
75
  const middlewareModel = (req, res, next) => {
53
76
  const modelname = req.params.modelname;
54
77
  req.modelname = modelname;
55
78
  req.Model = models[modelname];
56
79
  if (!req.Model) {
57
- throw new errors.NotFoundError(`Model ${modelname} not found`);
80
+ const err = new errors.NotFoundError(`Model ${modelname} not found`);
81
+ logRequestError(req, res, err, "model");
82
+ return next(err);
58
83
  }
59
84
  return next();
60
85
  };
@@ -62,7 +87,9 @@ const middlewarePasswords = (req, res, next) => {
62
87
  if (req.body && req.body.password) {
63
88
  if (req.query.password_override) {
64
89
  if (!res.user?.admin) {
65
- throw new errors.ForbiddenError("password_override requires admin");
90
+ const err = new errors.ForbiddenError("password_override requires admin");
91
+ logRequestError(req, res, err, "password_override");
92
+ return next(err);
66
93
  }
67
94
  }
68
95
  else {
@@ -94,7 +121,7 @@ const outputJSON = async (req, res) => {
94
121
  res.send(res.result);
95
122
  }
96
123
  catch (err) {
97
- console.error(new Date(), err);
124
+ logRequestError(req, res, err, "outputJSON");
98
125
  throw new errors.InternalServerError(safeErrorMessage(err));
99
126
  }
100
127
  };
@@ -118,7 +145,7 @@ const outputCSV = (req, res, next) => {
118
145
  next();
119
146
  }
120
147
  catch (err) {
121
- console.error(err);
148
+ logRequestError(req, res, err, "outputCSV");
122
149
  throw new errors.InternalServerError(safeErrorMessage(err));
123
150
  }
124
151
  };
@@ -132,8 +159,7 @@ const actionGet = async (req, res) => {
132
159
  filters = query_sanitize.sanitizeFilter(filters, getSecurityOpts(req));
133
160
  }
134
161
  catch (err) {
135
- console.trace(new Date(), err);
136
- // Preserve BadRequestError, convert others to InternalServerError
162
+ logRequestError(req, res, err, "filter");
137
163
  if (err instanceof errors.BadRequestError) {
138
164
  throw err;
139
165
  }
@@ -179,7 +205,7 @@ const actionGet = async (req, res) => {
179
205
  if (count >= 0) {
180
206
  result.count = count;
181
207
  }
182
- const effectiveLimit = query_limits.enforceListLimit(req, estimatedCount);
208
+ const effectiveLimit = query_limits.enforceListLimit(req, estimatedCount, res);
183
209
  query_limits.applyListPagination(q, result, req, effectiveLimit, count >= 0 ? count : 0, changeUrlParams);
184
210
  if (req.query.sort) {
185
211
  q.sort(req.query.sort);
@@ -224,10 +250,12 @@ const actionGet = async (req, res) => {
224
250
  console.timeEnd(opname);
225
251
  }
226
252
  catch (err) {
227
- console.error(new Date(), err);
228
253
  if (debug)
229
254
  console.timeEnd(opname);
230
- // Preserve BadRequestError, convert others to InternalServerError
255
+ if (!(err instanceof errors.BadRequestError) &&
256
+ !(err instanceof errors.ForbiddenError)) {
257
+ logRequestError(req, res, err, "get");
258
+ }
231
259
  if (err instanceof errors.BadRequestError) {
232
260
  throw err;
233
261
  }
@@ -249,7 +277,7 @@ const actionGetOne = async (req, res) => {
249
277
  console.timeEnd(opname);
250
278
  }
251
279
  catch (err) {
252
- console.error(new Date(), err);
280
+ logRequestError(req, res, err, "getOne");
253
281
  if (debug)
254
282
  console.timeEnd(opname);
255
283
  if (err.code)
@@ -284,7 +312,7 @@ const actionPost = async (req, res) => {
284
312
  console.timeEnd(opname);
285
313
  }
286
314
  catch (err) {
287
- console.error(new Date(), err);
315
+ logRequestError(req, res, err, "post");
288
316
  if (debug)
289
317
  console.timeEnd(opname);
290
318
  if (err.code)
@@ -298,8 +326,7 @@ const actionPut = async (req, res) => {
298
326
  try {
299
327
  let item = await req.Model.findById(req.params.item_id);
300
328
  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}`);
329
+ logAndThrow(req, res, new errors.NotFoundError(`Document ${req.params.item_id} not found on ${req.modelname}`), "put");
303
330
  }
304
331
  _populateItem(item, datamunging.deserialize(req.body));
305
332
  _versionItem(item);
@@ -324,7 +351,7 @@ const actionPut = async (req, res) => {
324
351
  console.timeEnd(opname);
325
352
  }
326
353
  catch (err) {
327
- console.error(new Date(), err);
354
+ logRequestError(req, res, err, "put");
328
355
  if (debug)
329
356
  console.timeEnd(opname);
330
357
  if (err.code)
@@ -364,7 +391,7 @@ const actionUpdate = async (req, res) => {
364
391
  console.timeEnd(opname);
365
392
  }
366
393
  catch (err) {
367
- console.error(new Date(), err);
394
+ logRequestError(req, res, err, "update");
368
395
  if (debug)
369
396
  console.timeEnd(opname);
370
397
  if (err.code)
@@ -398,7 +425,7 @@ const actionDelete = async (req, res) => {
398
425
  }
399
426
  }
400
427
  else {
401
- throw new errors.ConflictError(`Parent link item exists in ${linked_model.modelname}/${linked_model.field}`);
428
+ logAndThrow(req, res, new errors.ConflictError(`Parent link item exists in ${linked_model.modelname}/${linked_model.field}`), "delete");
402
429
  }
403
430
  }
404
431
  });
@@ -427,7 +454,9 @@ const actionDelete = async (req, res) => {
427
454
  console.timeEnd(opname);
428
455
  }
429
456
  catch (err) {
430
- console.error(new Date(), err);
457
+ if (!(err instanceof errors.ConflictError)) {
458
+ logRequestError(req, res, err, "delete");
459
+ }
431
460
  if (debug)
432
461
  console.timeEnd(opname);
433
462
  if (err.code)
@@ -444,7 +473,7 @@ const actionCount = async (req, res) => {
444
473
  filters = query_sanitize.sanitizeFilter(filters, getSecurityOpts(req));
445
474
  }
446
475
  catch (err) {
447
- console.trace(new Date(), err);
476
+ logRequestError(req, res, err, "filter");
448
477
  if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
449
478
  throw err;
450
479
  }
@@ -464,7 +493,7 @@ const actionCount = async (req, res) => {
464
493
  console.timeEnd(opname);
465
494
  }
466
495
  catch (err) {
467
- console.error(new Date(), err);
496
+ logRequestError(req, res, err, "count");
468
497
  if (debug)
469
498
  console.timeEnd(opname);
470
499
  if (err.code)
@@ -481,7 +510,7 @@ const actionCall = async (req, res) => {
481
510
  res.json(result);
482
511
  }
483
512
  catch (err) {
484
- console.error(new Date(), err);
513
+ logRequestError(req, res, err, "call");
485
514
  if (err.code)
486
515
  throw err;
487
516
  if (err instanceof errors.ForbiddenError || err instanceof errors.NotFoundError) {
@@ -506,7 +535,7 @@ const actionCallItem = async (req, res) => {
506
535
  res.json(result);
507
536
  }
508
537
  catch (err) {
509
- console.trace(err);
538
+ logRequestError(req, res, err, "call");
510
539
  if (err.code)
511
540
  throw err;
512
541
  if (err instanceof errors.ForbiddenError || err instanceof errors.NotFoundError) {
@@ -517,15 +546,22 @@ const actionCallItem = async (req, res) => {
517
546
  };
518
547
  // Actions (verbs)
519
548
  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
549
  if (!req.body || !req.body.query || typeof req.body.query !== "object") {
524
- throw new errors.BadRequestError("Query missing or not of type object");
550
+ logAndThrow(req, res, new errors.BadRequestError("Query missing or not of type object"), "query");
525
551
  }
526
552
  const opname = `query ${req.modelname} ${ops++}`;
527
553
  console.time(opname);
528
- const sanitizedQuery = query_sanitize.sanitizeFilter(req.body.query, getSecurityOpts(req));
554
+ let sanitizedQuery;
555
+ try {
556
+ sanitizedQuery = query_sanitize.sanitizeFilter(req.body.query, getSecurityOpts(req));
557
+ }
558
+ catch (err) {
559
+ logRequestError(req, res, err, "query_sanitize");
560
+ if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
561
+ throw err;
562
+ }
563
+ throw new errors.InternalServerError(safeErrorMessage(err));
564
+ }
529
565
  let query = [sanitizedQuery];
530
566
  let checkDeleted = { "$or": [{ _deleted: false }, { _deleted: null }] };
531
567
  if (!req.query.showDeleted) {
@@ -543,7 +579,7 @@ const actionQuery = async (req, res) => {
543
579
  result.count = count;
544
580
  }
545
581
  const estimatedCount = await req.Model.estimatedDocumentCount();
546
- const effectiveLimit = query_limits.enforceListLimit(req, estimatedCount);
582
+ const effectiveLimit = query_limits.enforceListLimit(req, estimatedCount, res);
547
583
  query_limits.applyListPagination(q, result, req, effectiveLimit, count >= 0 ? count : 0, changeUrlParams);
548
584
  if (req.query.sort) {
549
585
  q.sort(req.query.sort);
@@ -585,9 +621,12 @@ const actionQuery = async (req, res) => {
585
621
  res.json(result);
586
622
  }
587
623
  catch (err) {
588
- console.error(new Date(), err);
589
624
  if (debug)
590
625
  console.timeEnd(opname);
626
+ if (!(err instanceof errors.BadRequestError) &&
627
+ !(err instanceof errors.ForbiddenError)) {
628
+ logRequestError(req, res, err, "query");
629
+ }
591
630
  if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
592
631
  throw err;
593
632
  }
@@ -598,19 +637,24 @@ const actionQuery = async (req, res) => {
598
637
  };
599
638
  // Actions (verbs)
600
639
  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
640
+ let query = req.body?.query ? req.body.query : req.body;
605
641
  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");
642
+ logAndThrow(req, res, new errors.BadRequestError("Query missing or not of type array"), "aggregate");
608
643
  }
609
644
  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
- });
645
+ try {
646
+ aggregate_guard.validatePipeline(query, {
647
+ aggregate_stages_allow: getSecurityOpts(req).aggregate_stages_allow,
648
+ isAdmin: res.user?.admin,
649
+ });
650
+ }
651
+ catch (err) {
652
+ logRequestError(req, res, err, "aggregate_guard");
653
+ if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
654
+ throw err;
655
+ }
656
+ throw new errors.InternalServerError(safeErrorMessage(err));
657
+ }
614
658
  const opname = `aggregate ${req.modelname} ${ops++}`;
615
659
  console.time(opname);
616
660
  try {
@@ -628,9 +672,12 @@ const actionAggregate = async (req, res) => {
628
672
  res.json(result);
629
673
  }
630
674
  catch (err) {
631
- console.error(new Date(), err);
632
675
  if (debug)
633
676
  console.timeEnd(opname);
677
+ if (!(err instanceof errors.BadRequestError) &&
678
+ !(err instanceof errors.ForbiddenError)) {
679
+ logRequestError(req, res, err, "aggregate");
680
+ }
634
681
  if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
635
682
  throw err;
636
683
  }
@@ -639,12 +686,10 @@ const actionAggregate = async (req, res) => {
639
686
  };
640
687
  // Actions (verbs)
641
688
  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
689
  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");
690
+ const err = new errors.BadRequestError("Query missing or not of type array");
691
+ logRequestError(req, res, err, "bulkwrite");
692
+ throw err;
648
693
  }
649
694
  const opname = `bulkwrite ${req.modelname} ${ops++}`;
650
695
  console.time(opname);
@@ -662,7 +707,7 @@ const actionBulkWrite = async (req, res) => {
662
707
  res.json(result);
663
708
  }
664
709
  catch (err) {
665
- console.error(new Date(), err);
710
+ logRequestError(req, res, err, "bulkwrite");
666
711
  if (debug)
667
712
  console.timeEnd(opname);
668
713
  if (err instanceof errors.BadRequestError || err instanceof errors.ForbiddenError) {
@@ -734,7 +779,6 @@ const getOne = async (Model, item_id, params, options) => {
734
779
  return response_sanitize.sanitizeDocument(item);
735
780
  }
736
781
  catch (err) {
737
- console.error(err);
738
782
  if (err.code)
739
783
  throw err;
740
784
  throw new errors.InternalServerError(safeErrorMessage(err));
@@ -1053,6 +1097,10 @@ const JXP = function (options) {
1053
1097
  ws.init({ models });
1054
1098
  cache.init(config);
1055
1099
  const docs = new Docs({ config, models });
1100
+ docsAuth.init(config);
1101
+ docsAuth.logDocsAccessMode(config);
1102
+ const loginThrottle = loginRateLimit.createLoginThrottle(config);
1103
+ loginRateLimit.logLoginRateLimit(config);
1056
1104
  // Set up our API server
1057
1105
  // Rate limitting
1058
1106
  if (config.throttle) {
@@ -1108,9 +1156,9 @@ const JXP = function (options) {
1108
1156
  // CSV endpoints
1109
1157
  server.get("/csv/:modelname", middlewareModel, security.login, security.auth, config.pre_hooks.get, actionGet, outputCSV);
1110
1158
  // 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);
1159
+ server.post("/query/:modelname", middlewareModel, security.login, security.auth, middlewareAdvancedQueryAllowed("query"), config.pre_hooks.get, actionQuery);
1160
+ server.post("/aggregate/:modelname", middlewareModel, security.login, security.auth, middlewareAdvancedQueryAllowed("aggregate"), config.pre_hooks.get, actionAggregate);
1161
+ server.post("/bulkwrite/:modelname", middlewareModel, security.login, middlewareBulkWriteAllowed, security.bulkAuth, config.pre_hooks.get, actionBulkWrite);
1114
1162
  server.post("/update/:modelname/:item_id", middlewareModel, security.login, security.auth, middlewarePasswords, middlewareCheckAdmin, config.pre_hooks.update, actionUpdate, cache.clearAll);
1115
1163
  /* Batch routes - ROLLED BACK FOR NOW */
1116
1164
  // server.post('/batch/create/:modelname', middlewareModel, security.login, security.auth, actionBatch);
@@ -1125,7 +1173,14 @@ const JXP = function (options) {
1125
1173
  server.get("/logout", security.login, login.logout);
1126
1174
  server.get("/login/oauth/:provider", login.oauth);
1127
1175
  server.get("/login/oauth/callback/:provider", login.oauth_callback);
1128
- server.post("/login", config.pre_hooks.login, login.login, config.post_hooks.login, outputJSON);
1176
+ const loginChain = [
1177
+ ...(loginThrottle ? [loginThrottle] : []),
1178
+ config.pre_hooks.login,
1179
+ login.login,
1180
+ config.post_hooks.login,
1181
+ outputJSON,
1182
+ ];
1183
+ server.post("/login", ...loginChain);
1129
1184
  server.post("/refresh", security.refresh);
1130
1185
  server.post("/login/refresh", security.refresh);
1131
1186
  /* Groups */
@@ -1137,10 +1192,16 @@ const JXP = function (options) {
1137
1192
  server.get("/model/:modelname", middlewareModel, docs.metaModel.bind(docs));
1138
1193
  server.get("/model", docs.metaModels.bind(docs));
1139
1194
  // server.get("/docs/_design", docs.dbDiagram.bind(docs));
1195
+ server.get("/docs/login", async (req, res) => {
1196
+ await docsAuth.loginPage(req, res, docs.renderLogin.bind(docs));
1197
+ });
1198
+ server.post("/docs/session", ...(loginThrottle ? [loginThrottle] : []), docsAuth.establishSession);
1199
+ server.get("/docs/session", docsAuth.getSession);
1200
+ server.post("/docs/logout", docsAuth.logout);
1140
1201
  server.get("/docs/assets/:file", docs.serveAsset.bind(docs));
1141
- server.get("/docs/api", docs.apiIndex.bind(docs));
1202
+ server.get("/docs/api", docsAuth.docsAccessMiddleware, docs.apiIndex.bind(docs));
1142
1203
  server.get("/docs/md/:md_doc", docs.md.bind(docs));
1143
- server.get("/docs/model/:modelname", docs.model.bind(docs));
1204
+ server.get("/docs/model/:modelname", docsAuth.docsAccessMiddleware, docs.model.bind(docs));
1144
1205
  server.get("/", docs.frontPage.bind(docs));
1145
1206
  /* Setup */
1146
1207
  server.get("/setup", setup.checkUserDoesNotExist, setup.setup);
@@ -1148,6 +1209,10 @@ const JXP = function (options) {
1148
1209
  server.post("/setup/data", setup.checkUserDoesNotExist, setup.data_setup);
1149
1210
  /* Websocket */
1150
1211
  server.on("upgrade", ws.upgrade);
1212
+ server.on("restError", (req, res, err, callback) => {
1213
+ logRequestError(req, res, err);
1214
+ return callback();
1215
+ });
1151
1216
  /* Cache */
1152
1217
  server.get("/cache/stats", security.login, security.admin_only, cache.stats, outputJSON);
1153
1218
  server.get("/cache/clear", security.login, security.admin_only, cache.clearAll, outputJSON);