@rvoh/psychic 1.6.4 → 1.7.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 (34) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/cjs/src/controller/helpers/renderDreamOrViewModel.js +22 -0
  3. package/dist/cjs/src/controller/index.js +33 -75
  4. package/dist/cjs/src/helpers/sanitizeString.js +28 -0
  5. package/dist/cjs/src/helpers/toJson.js +21 -0
  6. package/dist/cjs/src/index.js +8 -6
  7. package/dist/cjs/src/psychic-app/helpers/import/importControllers.js +1 -4
  8. package/dist/cjs/src/psychic-app/index.js +45 -44
  9. package/dist/cjs/src/psychic-app/openapi-cache.js +11 -53
  10. package/dist/cjs/src/router/index.js +9 -15
  11. package/dist/cjs/src/server/index.js +17 -21
  12. package/dist/esm/src/controller/helpers/renderDreamOrViewModel.js +19 -0
  13. package/dist/esm/src/controller/index.js +31 -72
  14. package/dist/esm/src/helpers/sanitizeString.js +25 -0
  15. package/dist/esm/src/helpers/toJson.js +15 -0
  16. package/dist/esm/src/index.js +3 -2
  17. package/dist/esm/src/psychic-app/helpers/import/importControllers.js +1 -4
  18. package/dist/esm/src/psychic-app/index.js +46 -45
  19. package/dist/esm/src/psychic-app/openapi-cache.js +8 -27
  20. package/dist/esm/src/router/index.js +9 -15
  21. package/dist/esm/src/server/index.js +17 -21
  22. package/dist/types/src/controller/helpers/renderDreamOrViewModel.d.ts +2 -0
  23. package/dist/types/src/controller/index.d.ts +3 -30
  24. package/dist/types/src/helpers/sanitizeString.d.ts +1 -0
  25. package/dist/types/src/helpers/toJson.d.ts +1 -0
  26. package/dist/types/src/index.d.ts +3 -2
  27. package/dist/types/src/psychic-app/index.d.ts +24 -27
  28. package/dist/types/src/psychic-app/openapi-cache.d.ts +4 -3
  29. package/dist/types/src/router/index.d.ts +2 -5
  30. package/dist/types/src/server/index.d.ts +1 -2
  31. package/package.json +2 -2
  32. package/dist/cjs/src/error/openapi/OpenapiFileNotFound.js +0 -27
  33. package/dist/esm/src/error/openapi/OpenapiFileNotFound.js +0 -24
  34. package/dist/types/src/error/openapi/OpenapiFileNotFound.d.ts +0 -6
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 1.7.1
2
+
3
+ - compute openapi doc during intiialization, rather than problematically reading from a file cache
4
+
5
+ ## 1.7.0
6
+
7
+ - `sanitizeResponseJson` config to automatically escape `<`, `>`, `&`, `/`, `\`, `'`, and `"` unicode representations when rendering json to satisfy security reviews (e.g., a pentest report recently called this out on one of our applications). For all practical purposes, this doesn't protect against anything (now that we have the `nosniff` header) since `JSON.parse` on the other end restores the original, dangerous string. Modern front end web frameworks already handle safely displaying arbitrary content, so further sanitization generally isn't needed. This version does provide the `sanitizeString` function that could be used to sanitize individual strings, replacing the above characters with string representations of the unicode characters that will survive Psychic converting to json and then parsing that json (i.e.: `<` will end up as the string "\u003c")
8
+
9
+ - Fix openapi serializer fallback issue introduced in 1.6.3, where we mistakenly double render data that has already been serialized.
10
+
1
11
  ## 1.6.4
2
12
 
3
13
  Raise an exception if attempting to import an openapi file during PsychicApp.init when in production. We will still swallow the exception in non-prod environments so that one can create a new openapi configuration and run sync without getting an error.
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = renderDreamOrVewModel;
4
+ const dream_1 = require("@rvoh/dream");
5
+ function renderDreamOrVewModel(data, serializerKey,
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ passthrough, renderOpts) {
8
+ const serializer = (0, dream_1.inferSerializerFromDreamOrViewModel)(data, serializerKey);
9
+ if (serializer && (0, dream_1.isDreamSerializer)(serializer)) {
10
+ // passthrough data going into the serializer is the argument that gets
11
+ // used in the custom attribute callback function
12
+ return serializer(data, passthrough).render(
13
+ // passthrough data must be passed both into the serializer and render
14
+ // because, if the serializer does accept passthrough data, then passing it in is how
15
+ // it gets into the serializer, but if it does not accept passthrough data, and therefore
16
+ // does not pass it into the call to DreamSerializer/ObjectSerializer,
17
+ // then it would be lost to serializers rendered via rendersOne/Many, and SerializerRenderer
18
+ // handles passing its passthrough data into those
19
+ passthrough, renderOpts);
20
+ }
21
+ throw new Error(`${serializer?.constructor?.name} is not a Dream serializer`);
22
+ }
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.controllerSerializerIndex = exports.ControllerSerializerIndex = exports.PsychicParamsPrimitiveLiterals = void 0;
6
+ exports.PsychicParamsPrimitiveLiterals = void 0;
7
7
  const dream_1 = require("@rvoh/dream");
8
8
  const ParamValidationError_js_1 = __importDefault(require("../error/controller/ParamValidationError.js"));
9
9
  const BadGateway_js_1 = __importDefault(require("../error/http/BadGateway.js"));
@@ -37,10 +37,13 @@ const Unauthorized_js_1 = __importDefault(require("../error/http/Unauthorized.js
37
37
  const UnavailableForLegalReasons_js_1 = __importDefault(require("../error/http/UnavailableForLegalReasons.js"));
38
38
  const UnprocessableContent_js_1 = __importDefault(require("../error/http/UnprocessableContent.js"));
39
39
  const UnsupportedMediaType_js_1 = __importDefault(require("../error/http/UnsupportedMediaType.js"));
40
+ const toJson_js_1 = __importDefault(require("../helpers/toJson.js"));
40
41
  const OpenapiPayloadValidator_js_1 = __importDefault(require("../openapi-renderer/helpers/OpenapiPayloadValidator.js"));
42
+ const index_js_1 = __importDefault(require("../psychic-app/index.js"));
41
43
  const params_js_1 = __importDefault(require("../server/params.js"));
42
- const index_js_1 = __importDefault(require("../session/index.js"));
44
+ const index_js_2 = __importDefault(require("../session/index.js"));
43
45
  const isPaginatedResult_js_1 = __importDefault(require("./helpers/isPaginatedResult.js"));
46
+ const renderDreamOrViewModel_js_1 = __importDefault(require("./helpers/renderDreamOrViewModel.js"));
44
47
  exports.PsychicParamsPrimitiveLiterals = [
45
48
  'bigint',
46
49
  'bigint[]',
@@ -108,26 +111,6 @@ class PsychicController {
108
111
  */
109
112
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
113
  static openapi;
111
- /**
112
- * Enables you to specify specific serializers to use
113
- * when encountering specific models, i.e.
114
- *
115
- * ```ts
116
- * class MyController extends AuthedController {
117
- * static {
118
- * this.serializes(User).with(UserCustomSerializer)
119
- * }
120
- * }
121
- * ````
122
- */
123
- static serializes(ModelClass) {
124
- return {
125
- with: (SerializerClass) => {
126
- exports.controllerSerializerIndex.add(this, SerializerClass, ModelClass);
127
- return this;
128
- },
129
- };
130
- }
131
114
  /**
132
115
  * @internal
133
116
  *
@@ -192,14 +175,12 @@ class PsychicController {
192
175
  req;
193
176
  res;
194
177
  session;
195
- config;
196
178
  action;
197
179
  renderOpts;
198
- constructor(req, res, { config, action, }) {
180
+ constructor(req, res, { action, }) {
199
181
  this.req = req;
200
182
  this.res = res;
201
- this.config = config;
202
- this.session = new index_js_1.default(req, res);
183
+ this.session = new index_js_2.default(req, res);
203
184
  this.action = action;
204
185
  // TODO: read casing from Dream app config
205
186
  this.renderOpts = {
@@ -357,13 +338,13 @@ class PsychicController {
357
338
  return this.session.setCookie(name, data, opts);
358
339
  }
359
340
  startSession(user) {
360
- return this.setCookie(this.config.sessionCookieName, JSON.stringify({
341
+ return this.setCookie(index_js_1.default.getOrFail().sessionCookieName, JSON.stringify({
361
342
  id: user.primaryKeyValue().toString(),
362
343
  modelKey: user.constructor.globalName,
363
344
  }));
364
345
  }
365
346
  endSession() {
366
- return this.session.clearCookie(this.config.sessionCookieName);
347
+ return this.session.clearCookie(index_js_1.default.getOrFail().sessionCookieName);
367
348
  }
368
349
  singleObjectJson(data, opts) {
369
350
  if (!data)
@@ -373,8 +354,6 @@ class PsychicController {
373
354
  if (data instanceof dream_1.DreamSerializerBuilder || data instanceof dream_1.ObjectSerializerBuilder) {
374
355
  return data.render(this.defaultSerializerPassthrough, this.renderOpts);
375
356
  }
376
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
377
- const lookup = exports.controllerSerializerIndex.lookupModel(this.constructor, data.constructor);
378
357
  const openapiDef = this.constructor?.openapi?.[this.action];
379
358
  // passthrough data must be passed both into the serializer and render
380
359
  // because, if the serializer does accept passthrough data, then passing it in is how
@@ -383,48 +362,38 @@ class PsychicController {
383
362
  // then it would be lost to serializers rendered via rendersOne/Many, and SerializerRenderer
384
363
  // handles passing its passthrough data into those
385
364
  const passthrough = this.defaultSerializerPassthrough;
386
- if (lookup?.length) {
387
- const serializer = lookup?.[1];
388
- if ((0, dream_1.isDreamSerializer)(serializer)) {
389
- // passthrough data going into the serializer is the argument that gets
390
- // used in the custom attribute callback function
365
+ if (data instanceof dream_1.Dream || data.serializers) {
366
+ if (!opts.serializerKey && (0, dream_1.isDreamSerializer)(openapiDef?.dreamsOrSerializers)) {
367
+ const serializer = openapiDef.dreamsOrSerializers;
391
368
  return serializer(data, passthrough).render(passthrough, this.renderOpts);
392
369
  }
393
- }
394
- else if ((0, dream_1.isDreamSerializer)(openapiDef?.dreamsOrSerializers)) {
395
- const serializer = openapiDef.dreamsOrSerializers;
396
- return serializer(data, passthrough).render(passthrough, this.renderOpts);
397
- }
398
- else if (data instanceof dream_1.Dream || data.serializers) {
399
- const serializer = (0, dream_1.inferSerializerFromDreamOrViewModel)(data, opts.serializerKey ||
370
+ return (0, renderDreamOrViewModel_js_1.default)(data, opts.serializerKey ||
400
371
  psychicControllerClass['controllerActionMetadata'][this.action]?.['serializerKey'] ||
401
- 'default');
402
- if (serializer && (0, dream_1.isDreamSerializer)(serializer)) {
403
- // passthrough data going into the serializer is the argument that gets
404
- // used in the custom attribute callback function
405
- return serializer(data, passthrough).render(
406
- // passthrough data must be passed both into the serializer and render
407
- // because, if the serializer does accept passthrough data, then passing it in is how
408
- // it gets into the serializer, but if it does not accept passthrough data, and therefore
409
- // does not pass it into the call to DreamSerializer/ObjectSerializer,
410
- // then it would be lost to serializers rendered via rendersOne/Many, and SerializerRenderer
411
- // handles passing its passthrough data into those
412
- passthrough, this.renderOpts);
413
- }
372
+ 'default', passthrough, this.renderOpts);
373
+ }
374
+ else {
375
+ return data;
414
376
  }
415
- return data;
416
377
  }
417
378
  json(data, opts = {}) {
418
- if (Array.isArray(data))
419
- return this.validateAndRenderJsonResponse(data.map(d =>
379
+ return this.validateAndRenderJsonResponse(this._json(data, opts));
380
+ }
381
+ _json(data, opts = {}) {
382
+ if (Array.isArray(data)) {
420
383
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
421
- this.singleObjectJson(d, opts)));
422
- if ((0, isPaginatedResult_js_1.default)(data))
423
- return this.validateAndRenderJsonResponse({
384
+ return data.map(d => this.singleObjectJson(d, opts));
385
+ //
386
+ }
387
+ else if ((0, isPaginatedResult_js_1.default)(data)) {
388
+ return {
424
389
  ...data,
425
390
  results: data.results.map(result => this.singleObjectJson(result, opts)),
426
- });
427
- return this.validateAndRenderJsonResponse(this.singleObjectJson(data, opts));
391
+ };
392
+ //
393
+ }
394
+ else {
395
+ return this.singleObjectJson(data, opts);
396
+ }
428
397
  }
429
398
  /**
430
399
  * Runs the data through openapi response validation, and then renders
@@ -436,7 +405,7 @@ class PsychicController {
436
405
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
437
406
  data) {
438
407
  this.validateOpenapiResponseBody(data);
439
- this.res.json(data);
408
+ this.res.type('json').send((0, toJson_js_1.default)(data, index_js_1.default.getOrFail().sanitizeResponseJson));
440
409
  }
441
410
  defaultSerializerPassthrough = {};
442
411
  serializerPassthrough(passthrough) {
@@ -795,14 +764,3 @@ class PsychicController {
795
764
  }
796
765
  }
797
766
  exports.default = PsychicController;
798
- class ControllerSerializerIndex {
799
- associations = [];
800
- add(ControllerClass, SerializerClass, ModelClass) {
801
- this.associations.push([ControllerClass, SerializerClass, ModelClass]);
802
- }
803
- lookupModel(ControllerClass, ModelClass) {
804
- return this.associations.find(association => association[0] === ControllerClass && association[2] === ModelClass);
805
- }
806
- }
807
- exports.ControllerSerializerIndex = ControllerSerializerIndex;
808
- exports.controllerSerializerIndex = new ControllerSerializerIndex();
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = sanitizeString;
4
+ const CHARACTERS_TO_SANITIZE_REGEXP = /[\\&<>/'"]/g;
5
+ function sanitizeString(str) {
6
+ if (str === null || str === undefined)
7
+ return str;
8
+ return str.replace(CHARACTERS_TO_SANITIZE_REGEXP, function (char) {
9
+ switch (char) {
10
+ case '\\':
11
+ return '\\u005c';
12
+ case '/':
13
+ return '\\u002f';
14
+ case '<':
15
+ return '\\u003c';
16
+ case '>':
17
+ return '\\u003e';
18
+ case '&':
19
+ return '\\u0026';
20
+ case "'":
21
+ return '\\u0027';
22
+ case '"':
23
+ return '\\u0022';
24
+ default:
25
+ return char;
26
+ }
27
+ });
28
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = toJson;
7
+ const sanitizeString_js_1 = __importDefault(require("./sanitizeString.js"));
8
+ function toJson(data, sanitize) {
9
+ /**
10
+ * 'undefined' is invalid json, and `JSON.stringify(undefined)` returns `undefined`
11
+ * so follow the pattern established by Express and return '{}' for `undefined`
12
+ */
13
+ if (data === undefined)
14
+ return '{}';
15
+ if (sanitize) {
16
+ return JSON.stringify(data, (_, x) => (typeof x !== 'string' ? x : (0, sanitizeString_js_1.default)(x))).replace(/\\\\/g, '\\');
17
+ }
18
+ else {
19
+ return JSON.stringify(data);
20
+ }
21
+ }
@@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Params = exports.PsychicServer = exports.getPsychicHttpInstance = exports.PsychicRouter = exports.PsychicApp = exports.PsychicImporter = exports.MissingControllerActionPairingInRoutes = exports.pathifyNestedObject = exports.cookieMaxAgeFromCookieOpts = exports.generateResource = exports.generateController = exports.HttpStatusUnsupportedMediaType = exports.HttpStatusUnprocessableContent = exports.HttpStatusUnavailableForLegalReasons = exports.HttpStatusUnauthorized = exports.HttpStatusTooManyRequests = exports.HttpStatusServiceUnavailable = exports.HttpStatusRequestHeaderFieldsTooLarge = exports.HttpStatusProxyAuthenticationRequired = exports.HttpStatusPreconditionRequired = exports.HttpStatusPreconditionFailed = exports.HttpStatusPaymentRequired = exports.HttpStatusNotImplemented = exports.HttpStatusNotFound = exports.HttpStatusNotExtended = exports.HttpStatusNotAcceptable = exports.HttpStatusMisdirectedRequest = exports.HttpStatusMethodNotAllowed = exports.HttpStatusLocked = exports.HttpStatusInternalServerError = exports.HttpStatusInsufficientStorage = exports.HttpStatusImATeapot = exports.HttpStatusGone = exports.HttpStatusGatewayTimeout = exports.HttpStatusForbidden = exports.HttpStatusFailedDependency = exports.HttpStatusExpectationFailed = exports.HttpStatusContentTooLarge = exports.HttpStatusConflict = exports.HttpStatusBadRequest = exports.HttpStatusBadGateway = exports.I18nProvider = exports.envLoader = exports.PsychicDevtools = exports.PsychicController = exports.OpenAPI = exports.BeforeAction = exports.PsychicCLI = exports.PsychicBin = exports.pluralize = void 0;
7
- exports.PsychicSession = exports.ParamValidationErrors = exports.ParamValidationError = void 0;
6
+ exports.PsychicRouter = exports.PsychicApp = exports.PsychicImporter = exports.MissingControllerActionPairingInRoutes = exports.ParamValidationErrors = exports.ParamValidationError = exports.sanitizeString = exports.pathifyNestedObject = exports.cookieMaxAgeFromCookieOpts = exports.generateResource = exports.generateController = exports.HttpStatusUnsupportedMediaType = exports.HttpStatusUnprocessableContent = exports.HttpStatusUnavailableForLegalReasons = exports.HttpStatusUnauthorized = exports.HttpStatusTooManyRequests = exports.HttpStatusServiceUnavailable = exports.HttpStatusRequestHeaderFieldsTooLarge = exports.HttpStatusProxyAuthenticationRequired = exports.HttpStatusPreconditionRequired = exports.HttpStatusPreconditionFailed = exports.HttpStatusPaymentRequired = exports.HttpStatusNotImplemented = exports.HttpStatusNotFound = exports.HttpStatusNotExtended = exports.HttpStatusNotAcceptable = exports.HttpStatusMisdirectedRequest = exports.HttpStatusMethodNotAllowed = exports.HttpStatusLocked = exports.HttpStatusInternalServerError = exports.HttpStatusInsufficientStorage = exports.HttpStatusImATeapot = exports.HttpStatusGone = exports.HttpStatusGatewayTimeout = exports.HttpStatusForbidden = exports.HttpStatusFailedDependency = exports.HttpStatusExpectationFailed = exports.HttpStatusContentTooLarge = exports.HttpStatusConflict = exports.HttpStatusBadRequest = exports.HttpStatusBadGateway = exports.I18nProvider = exports.envLoader = exports.PsychicDevtools = exports.PsychicController = exports.OpenAPI = exports.BeforeAction = exports.PsychicCLI = exports.PsychicBin = exports.pluralize = void 0;
7
+ exports.PsychicSession = exports.Params = exports.PsychicServer = exports.getPsychicHttpInstance = void 0;
8
8
  const pluralize_esm_1 = __importDefault(require("pluralize-esm"));
9
9
  exports.pluralize = pluralize_esm_1.default;
10
10
  var index_js_1 = require("./bin/index.js");
@@ -90,6 +90,12 @@ var cookieMaxAgeFromCookieOpts_js_1 = require("./helpers/cookieMaxAgeFromCookieO
90
90
  Object.defineProperty(exports, "cookieMaxAgeFromCookieOpts", { enumerable: true, get: function () { return __importDefault(cookieMaxAgeFromCookieOpts_js_1).default; } });
91
91
  var pathifyNestedObject_js_1 = require("./helpers/pathifyNestedObject.js");
92
92
  Object.defineProperty(exports, "pathifyNestedObject", { enumerable: true, get: function () { return __importDefault(pathifyNestedObject_js_1).default; } });
93
+ var sanitizeString_js_1 = require("./helpers/sanitizeString.js");
94
+ Object.defineProperty(exports, "sanitizeString", { enumerable: true, get: function () { return __importDefault(sanitizeString_js_1).default; } });
95
+ var ParamValidationError_js_1 = require("./error/controller/ParamValidationError.js");
96
+ Object.defineProperty(exports, "ParamValidationError", { enumerable: true, get: function () { return __importDefault(ParamValidationError_js_1).default; } });
97
+ var ParamValidationErrors_js_1 = require("./error/controller/ParamValidationErrors.js");
98
+ Object.defineProperty(exports, "ParamValidationErrors", { enumerable: true, get: function () { return __importDefault(ParamValidationErrors_js_1).default; } });
93
99
  var endpoint_js_1 = require("./openapi-renderer/endpoint.js");
94
100
  Object.defineProperty(exports, "MissingControllerActionPairingInRoutes", { enumerable: true, get: function () { return endpoint_js_1.MissingControllerActionPairingInRoutes; } });
95
101
  var PsychicImporter_js_1 = require("./psychic-app/helpers/PsychicImporter.js");
@@ -104,9 +110,5 @@ var index_js_6 = require("./server/index.js");
104
110
  Object.defineProperty(exports, "PsychicServer", { enumerable: true, get: function () { return __importDefault(index_js_6).default; } });
105
111
  var params_js_1 = require("./server/params.js");
106
112
  Object.defineProperty(exports, "Params", { enumerable: true, get: function () { return __importDefault(params_js_1).default; } });
107
- var ParamValidationError_js_1 = require("./error/controller/ParamValidationError.js");
108
- Object.defineProperty(exports, "ParamValidationError", { enumerable: true, get: function () { return __importDefault(ParamValidationError_js_1).default; } });
109
- var ParamValidationErrors_js_1 = require("./error/controller/ParamValidationErrors.js");
110
- Object.defineProperty(exports, "ParamValidationErrors", { enumerable: true, get: function () { return __importDefault(ParamValidationErrors_js_1).default; } });
111
113
  var index_js_7 = require("./session/index.js");
112
114
  Object.defineProperty(exports, "PsychicSession", { enumerable: true, get: function () { return __importDefault(index_js_7).default; } });
@@ -40,10 +40,7 @@ importCb) {
40
40
  * at decoration time such that the class of a property being decorated is only avilable during instance instantiation. In order
41
41
  * to only apply static values once, on boot, `globallyInitializingDecorators` is set to true on Dream, and all Dream models are instantiated.
42
42
  */
43
- new controllerClass({}, {}, {
44
- action: 'a',
45
- config: psychicApp,
46
- });
43
+ new controllerClass({}, {}, { action: 'a' });
47
44
  }
48
45
  }
49
46
  index_js_1.default['globallyInitializingDecorators'] = false;
@@ -97,7 +97,10 @@ class PsychicApp {
97
97
  dreamApp.set('logger', psychicApp.logger);
98
98
  dreamApp.set('packageManager', psychicApp.packageManager);
99
99
  (0, cache_js_1.cachePsychicApp)(psychicApp);
100
- await Promise.all([psychicApp.buildOpenapiCache(), psychicApp.buildRoutesCache()]);
100
+ // routes _must_ be built before openapi
101
+ // cache can be processed
102
+ await psychicApp.buildRoutesCache();
103
+ psychicApp.buildOpenapiCache();
101
104
  });
102
105
  return psychicApp;
103
106
  }
@@ -111,7 +114,7 @@ class PsychicApp {
111
114
  async buildRoutesCache() {
112
115
  if (this._routesCache)
113
116
  return;
114
- const r = new index_js_1.default(null, this);
117
+ const r = new index_js_1.default(null);
115
118
  await this.routesCb(r);
116
119
  this._routesCache = r.routes;
117
120
  }
@@ -128,20 +131,40 @@ class PsychicApp {
128
131
  * request validation can look to the route cache
129
132
  * instead of having to build it from scratch.
130
133
  */
131
- async buildOpenapiCache() {
132
- // build caches for all files that are registered for validation
133
- await Promise.all(Object.keys(this.openapi)
134
- .filter(key => this.openapi[key]?.validate)
135
- .map(async (openapiName) => {
136
- await this.cacheOpenapiFile(openapiName);
137
- }));
138
- // ignore all files that are not registered for validation
139
- Object.keys(this.openapi)
140
- .filter(key => !this.openapi[key]?.validate)
141
- .forEach(openapiName => {
142
- this.ignoreOpenapiFile(openapiName);
134
+ buildOpenapiCache() {
135
+ Object.keys(this.openapi).forEach(openapiName => {
136
+ if (this.openapi[openapiName]?.validate) {
137
+ this.cacheOpenapiDoc(openapiName);
138
+ }
139
+ else {
140
+ this.ignoreOpenapiDoc(openapiName);
141
+ }
143
142
  });
144
143
  }
144
+ /**
145
+ * @internal
146
+ *
147
+ * When PsychicApp.init is called, a cache is built for each openapiName
148
+ * registered within your app config. For each openapiName, Psychic will
149
+ * read compute and cache the openapi document contents for that openapiName.
150
+ * It does this to enable the validation engine to read component
151
+ * schemas accross the entire document to perform individual endpoint validation.
152
+ *
153
+ * @param openapiName - the openapiName you want to look up the openapi cache for
154
+ */
155
+ cacheOpenapiDoc(openapiName) {
156
+ (0, openapi_cache_js_1.cacheOpenapiDoc)(openapiName, this.routesCache);
157
+ }
158
+ /**
159
+ * indicates to the underlying cache that this openapi file is intentionally
160
+ * being ignored, so that future lookups for the file cache do not raise
161
+ * an exception
162
+ *
163
+ * @param openapiName - the openapiName to ignore
164
+ */
165
+ ignoreOpenapiDoc(openapiName) {
166
+ (0, openapi_cache_js_1.ignoreOpenapiDoc)(openapiName);
167
+ }
145
168
  /**
146
169
  * Since javascript is inherently vulnerable to circular dependencies,
147
170
  * this function provides a workaround by enabling you to dynamically
@@ -273,6 +296,10 @@ Try setting it to something valid, like:
273
296
  get saltRounds() {
274
297
  return this._saltRounds;
275
298
  }
299
+ _sanitizeResponseJson = false;
300
+ get sanitizeResponseJson() {
301
+ return this._sanitizeResponseJson;
302
+ }
276
303
  _packageManager;
277
304
  get packageManager() {
278
305
  return this._packageManager;
@@ -285,35 +312,6 @@ Try setting it to something valid, like:
285
312
  get routesCb() {
286
313
  return this._routesCb;
287
314
  }
288
- /**
289
- * @internal
290
- *
291
- * When PsychicApp.init is called, a cache is built for each openapiName
292
- * registered within your app config. For each openapiName, Psychic will
293
- * read the openapi file (unless it has not been written yet) and cache the
294
- * contents. It does this to enable the validation engine to read component
295
- * schemas accross the entire document to perform individual endpoint validation.
296
- *
297
- * This function is used to cache the contents of this file during initialization,
298
- * so that the validation engine can call down to the complementing method,
299
- * `getOpenapiFileOrFail`, which will return the contents of the document, or undefined
300
- * if it was not found.
301
- *
302
- * @param openapiName - the openapiName you want to look up the openapi cache for
303
- */
304
- async cacheOpenapiFile(openapiName) {
305
- await (0, openapi_cache_js_1.readAndCacheOpenapiFile)(openapiName);
306
- }
307
- /**
308
- * indicates to the underlying cache that this openapi file is intentionally
309
- * being ignored, so that future lookups for the file cache do not raise
310
- * an exception
311
- *
312
- * @param openapiName - the openapiName to ignore
313
- */
314
- ignoreOpenapiFile(openapiName) {
315
- (0, openapi_cache_js_1.ignoreOpenapiFile)(openapiName);
316
- }
317
315
  /**
318
316
  * @internal
319
317
  *
@@ -332,7 +330,7 @@ Try setting it to something valid, like:
332
330
  * @returns the scanned openapi document, or undefined if it could not be found.
333
331
  */
334
332
  getOpenapiFileOrFail(openapiName) {
335
- return (0, openapi_cache_js_1.getOpenapiFileOrFail)(openapiName);
333
+ return (0, openapi_cache_js_1.getCachedOpenapiDocOrFail)(openapiName);
336
334
  }
337
335
  /**
338
336
  * @returns the entire openapi config, which includes configurations for each endpoint, indexed by their openapiNames
@@ -564,6 +562,9 @@ Try setting it to something valid, like:
564
562
  case 'saltRounds':
565
563
  this._saltRounds = value;
566
564
  break;
565
+ case 'sanitizeResponseJson':
566
+ this._sanitizeResponseJson = value;
567
+ break;
567
568
  case 'openapi':
568
569
  this._openapi = {
569
570
  ...this.openapi,
@@ -1,39 +1,13 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
26
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
4
  };
28
5
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.getOpenapiFileOrFail = getOpenapiFileOrFail;
30
- exports.readAndCacheOpenapiFile = readAndCacheOpenapiFile;
31
- exports.ignoreOpenapiFile = ignoreOpenapiFile;
32
- const fs = __importStar(require("node:fs/promises"));
33
- const openapiJsonPath_js_1 = __importDefault(require("../helpers/openapiJsonPath.js"));
6
+ exports.getCachedOpenapiDocOrFail = getCachedOpenapiDocOrFail;
7
+ exports.cacheOpenapiDoc = cacheOpenapiDoc;
8
+ exports.ignoreOpenapiDoc = ignoreOpenapiDoc;
34
9
  const must_call_psychic_app_init_first_js_1 = __importDefault(require("../error/psychic-app/must-call-psychic-app-init-first.js"));
35
- const EnvInternal_js_1 = __importDefault(require("../helpers/EnvInternal.js"));
36
- const OpenapiFileNotFound_js_1 = __importDefault(require("../error/openapi/OpenapiFileNotFound.js"));
10
+ const app_js_1 = __importDefault(require("../openapi-renderer/app.js"));
37
11
  const _openapiData = {};
38
12
  /**
39
13
  * Raises an exception if readAndCacheOpenapiFile was not called
@@ -42,7 +16,7 @@ const _openapiData = {};
42
16
  * @param openapiName - the openapiName you wish to look up
43
17
  * @returns the cached openapi file, or undefined if it was not found
44
18
  */
45
- function getOpenapiFileOrFail(openapiName) {
19
+ function getCachedOpenapiDocOrFail(openapiName) {
46
20
  const val = _openapiData[openapiName];
47
21
  if (!val)
48
22
  throw new must_call_psychic_app_init_first_js_1.default();
@@ -59,29 +33,13 @@ function getOpenapiFileOrFail(openapiName) {
59
33
  *
60
34
  * @param openapiName - the openapiName you wish to look up
61
35
  */
62
- async function readAndCacheOpenapiFile(openapiName) {
36
+ function cacheOpenapiDoc(openapiName, routes) {
63
37
  if (_openapiData[openapiName])
64
38
  return;
65
- const openapiPath = (0, openapiJsonPath_js_1.default)(openapiName);
66
- try {
67
- const buffer = await fs.readFile(openapiPath);
68
- const openapiDoc = JSON.parse(buffer.toString());
69
- // we only cache the components from the openapi files,
70
- // since that is all that we currently need for validation,
71
- // which is the only thing leveraging these cached openapi files.
72
- _openapiData[openapiName] = openapiDoc?.components
73
- ? { components: openapiDoc.components }
74
- : FILE_DOES_NOT_EXIST;
75
- }
76
- catch {
77
- if (EnvInternal_js_1.default.isProduction) {
78
- throw new OpenapiFileNotFound_js_1.default(openapiName, openapiPath);
79
- }
80
- // if the openapi file is not generated yet and we are in our local
81
- // environment, we don't want to raise an exception, since it could
82
- // prevent someone from ever defining a new openapi configuration.
83
- _openapiData[openapiName] = FILE_DOES_NOT_EXIST;
84
- }
39
+ const openapiDoc = app_js_1.default._toObject(routes, openapiName);
40
+ _openapiData[openapiName] = openapiDoc?.components
41
+ ? { components: openapiDoc.components }
42
+ : FILE_DOES_NOT_EXIST;
85
43
  }
86
44
  /**
87
45
  * Reads the openapi file corresponding to the openapiName,
@@ -92,7 +50,7 @@ async function readAndCacheOpenapiFile(openapiName) {
92
50
  *
93
51
  * @param openapiName - the openapiName you wish to look up
94
52
  */
95
- function ignoreOpenapiFile(openapiName) {
53
+ function ignoreOpenapiDoc(openapiName) {
96
54
  _openapiData[openapiName] = FILE_WAS_IGNORED;
97
55
  }
98
56
  const FILE_DOES_NOT_EXIST = 'FILE_DOES_NOT_EXIST';
@@ -19,15 +19,10 @@ const route_manager_js_1 = __importDefault(require("./route-manager.js"));
19
19
  const types_js_1 = require("./types.js");
20
20
  class PsychicRouter {
21
21
  app;
22
- config;
23
22
  currentNamespaces = [];
24
23
  routeManager = new route_manager_js_1.default();
25
- constructor(app, config) {
24
+ constructor(app) {
26
25
  this.app = app;
27
- this.config = config;
28
- }
29
- get routingMechanism() {
30
- return this.app;
31
26
  }
32
27
  get routes() {
33
28
  return this.routeManager.routes;
@@ -115,13 +110,13 @@ suggested fix: "${(0, helpers_js_1.convertRouteParams)(path)}"
115
110
  `);
116
111
  }
117
112
  namespace(namespace, cb) {
118
- const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
113
+ const nestedRouter = new PsychicNestedRouter(this.app, this.routeManager, {
119
114
  namespaces: this.currentNamespaces,
120
115
  });
121
116
  this.runNestedCallbacks(namespace, nestedRouter, cb);
122
117
  }
123
118
  scope(scope, cb) {
124
- const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
119
+ const nestedRouter = new PsychicNestedRouter(this.app, this.routeManager, {
125
120
  namespaces: this.currentNamespaces,
126
121
  });
127
122
  this.runNestedCallbacks(scope, nestedRouter, cb, { treatNamespaceAsScope: true });
@@ -134,7 +129,7 @@ suggested fix: "${(0, helpers_js_1.convertRouteParams)(path)}"
134
129
  }
135
130
  collection(cb) {
136
131
  const replacedNamespaces = this.currentNamespaces.slice(0, this.currentNamespaces.length - 1);
137
- const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
132
+ const nestedRouter = new PsychicNestedRouter(this.app, this.routeManager, {
138
133
  namespaces: replacedNamespaces,
139
134
  });
140
135
  const currentNamespace = replacedNamespaces[replacedNamespaces.length - 1];
@@ -156,7 +151,7 @@ suggested fix: "${(0, helpers_js_1.convertRouteParams)(path)}"
156
151
  }
157
152
  }
158
153
  _makeResource(path, options, cb, plural) {
159
- const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
154
+ const nestedRouter = new PsychicNestedRouter(this.app, this.routeManager, {
160
155
  namespaces: this.currentNamespaces,
161
156
  });
162
157
  const { only, except } = options || {};
@@ -289,9 +284,9 @@ suggested fix: "${(0, helpers_js_1.convertRouteParams)(path)}"
289
284
  index_js_1.default.log('ATTENTION: a server error was detected:');
290
285
  index_js_1.default.logWithLevel('error', err);
291
286
  }
292
- if (this.config.specialHooks.serverError.length) {
287
+ if (index_js_1.default.getOrFail().specialHooks.serverError.length) {
293
288
  try {
294
- for (const hook of this.config.specialHooks.serverError) {
289
+ for (const hook of index_js_1.default.getOrFail().specialHooks.serverError) {
295
290
  await hook(err, req, res);
296
291
  }
297
292
  }
@@ -321,7 +316,6 @@ suggested fix: "${(0, helpers_js_1.convertRouteParams)(path)}"
321
316
  }
322
317
  _initializeController(ControllerClass, req, res, action) {
323
318
  return new ControllerClass(req, res, {
324
- config: this.config,
325
319
  action,
326
320
  });
327
321
  }
@@ -329,8 +323,8 @@ suggested fix: "${(0, helpers_js_1.convertRouteParams)(path)}"
329
323
  exports.default = PsychicRouter;
330
324
  class PsychicNestedRouter extends PsychicRouter {
331
325
  router;
332
- constructor(expressApp, config, routeManager, { namespaces = [], } = {}) {
333
- super(expressApp, config);
326
+ constructor(expressApp, routeManager, { namespaces = [], } = {}) {
327
+ super(expressApp);
334
328
  this.router = (0, express_1.Router)();
335
329
  this.currentNamespaces = namespaces;
336
330
  this.routeManager = routeManager;