@rvoh/psychic 1.6.4 → 1.7.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 1.7.0
2
+
3
+ - `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")
4
+
5
+ - Fix openapi serializer fallback issue introduced in 1.6.3, where we mistakenly double render data that has already been serialized.
6
+
1
7
  ## 1.6.4
2
8
 
3
9
  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;
@@ -111,7 +111,7 @@ class PsychicApp {
111
111
  async buildRoutesCache() {
112
112
  if (this._routesCache)
113
113
  return;
114
- const r = new index_js_1.default(null, this);
114
+ const r = new index_js_1.default(null);
115
115
  await this.routesCb(r);
116
116
  this._routesCache = r.routes;
117
117
  }
@@ -273,6 +273,10 @@ Try setting it to something valid, like:
273
273
  get saltRounds() {
274
274
  return this._saltRounds;
275
275
  }
276
+ _sanitizeResponseJson = false;
277
+ get sanitizeResponseJson() {
278
+ return this._sanitizeResponseJson;
279
+ }
276
280
  _packageManager;
277
281
  get packageManager() {
278
282
  return this._packageManager;
@@ -564,6 +568,9 @@ Try setting it to something valid, like:
564
568
  case 'saltRounds':
565
569
  this._saltRounds = value;
566
570
  break;
571
+ case 'sanitizeResponseJson':
572
+ this._sanitizeResponseJson = value;
573
+ break;
567
574
  case 'openapi':
568
575
  this._openapi = {
569
576
  ...this.openapi,
@@ -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;
@@ -30,10 +30,10 @@ const dream_1 = require("@rvoh/dream");
30
30
  const cookieParser = __importStar(require("cookie-parser"));
31
31
  const cors = __importStar(require("cors"));
32
32
  const express = __importStar(require("express"));
33
+ const EnvInternal_js_1 = __importDefault(require("../helpers/EnvInternal.js"));
33
34
  const index_js_1 = __importDefault(require("../psychic-app/index.js"));
34
35
  const index_js_2 = __importDefault(require("../router/index.js"));
35
36
  const startPsychicServer_js_1 = __importStar(require("./helpers/startPsychicServer.js"));
36
- const EnvInternal_js_1 = __importDefault(require("../helpers/EnvInternal.js"));
37
37
  // const debugEnabled = debuglog('psychic').enabled
38
38
  class PsychicServer {
39
39
  static async startPsychicServer(opts) {
@@ -51,13 +51,9 @@ class PsychicServer {
51
51
  constructor() {
52
52
  this.buildApp();
53
53
  }
54
- get config() {
55
- return index_js_1.default.getOrFail();
56
- }
57
54
  async routes() {
58
- const r = new index_js_2.default(this.expressApp, this.config);
59
- const psychicApp = index_js_1.default.getOrFail();
60
- await psychicApp.routesCb(r);
55
+ const r = new index_js_2.default(this.expressApp);
56
+ await index_js_1.default.getOrFail().routesCb(r);
61
57
  return r.routes;
62
58
  }
63
59
  async boot() {
@@ -71,13 +67,14 @@ class PsychicServer {
71
67
  });
72
68
  next();
73
69
  });
74
- for (const serverInitBeforeMiddlewareHook of this.config.specialHooks.serverInitBeforeMiddleware) {
70
+ for (const serverInitBeforeMiddlewareHook of index_js_1.default.getOrFail().specialHooks
71
+ .serverInitBeforeMiddleware) {
75
72
  await serverInitBeforeMiddlewareHook(this);
76
73
  }
77
74
  this.initializeCors();
78
75
  this.initializeJSON();
79
76
  try {
80
- await this.config.boot();
77
+ await index_js_1.default.getOrFail().boot();
81
78
  }
82
79
  catch (err) {
83
80
  const error = err;
@@ -87,11 +84,12 @@ class PsychicServer {
87
84
  ${error.message}
88
85
  `);
89
86
  }
90
- for (const serverInitAfterMiddlewareHook of this.config.specialHooks.serverInitAfterMiddleware) {
87
+ for (const serverInitAfterMiddlewareHook of index_js_1.default.getOrFail().specialHooks
88
+ .serverInitAfterMiddleware) {
91
89
  await serverInitAfterMiddlewareHook(this);
92
90
  }
93
91
  await this.buildRoutes();
94
- for (const afterRoutesHook of this.config.specialHooks.serverInitAfterRoutes) {
92
+ for (const afterRoutesHook of index_js_1.default.getOrFail().specialHooks.serverInitAfterRoutes) {
95
93
  await afterRoutesHook(this);
96
94
  }
97
95
  this.booted = true;
@@ -119,7 +117,7 @@ class PsychicServer {
119
117
  const httpServer = await (0, startPsychicServer_js_1.default)({
120
118
  app: this.expressApp,
121
119
  port: port || psychicApp.port,
122
- sslCredentials: this.config.sslCredentials,
120
+ sslCredentials: index_js_1.default.getOrFail().sslCredentials,
123
121
  });
124
122
  this.httpServer = httpServer;
125
123
  }
@@ -146,8 +144,7 @@ class PsychicServer {
146
144
  process.exit();
147
145
  }
148
146
  async stop({ bypassClosingDbConnections = false } = {}) {
149
- const psychicApp = index_js_1.default.getOrFail();
150
- for (const hook of psychicApp.specialHooks.serverShutdown) {
147
+ for (const hook of index_js_1.default.getOrFail().specialHooks.serverShutdown) {
151
148
  await hook(this);
152
149
  }
153
150
  this.httpServer?.close();
@@ -156,8 +153,7 @@ class PsychicServer {
156
153
  }
157
154
  }
158
155
  async serveForRequestSpecs(block) {
159
- const psychicApp = index_js_1.default.getOrFail();
160
- const port = psychicApp.port;
156
+ const port = index_js_1.default.getOrFail().port;
161
157
  await this.boot();
162
158
  let server;
163
159
  await new Promise(accept => {
@@ -172,16 +168,16 @@ class PsychicServer {
172
168
  this.expressApp.use(cookieParser.default());
173
169
  }
174
170
  initializeCors() {
171
+ this.expressApp.use(
175
172
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
- this.expressApp.use(cors.default(this.config.corsOptions));
173
+ cors.default(index_js_1.default.getOrFail().corsOptions));
177
174
  }
178
175
  initializeJSON() {
179
- this.expressApp.use(express.json(this.config.jsonOptions));
176
+ this.expressApp.use(express.json(index_js_1.default.getOrFail().jsonOptions));
180
177
  }
181
178
  async buildRoutes() {
182
- const r = new index_js_2.default(this.expressApp, this.config);
183
- const psychicApp = index_js_1.default.getOrFail();
184
- await psychicApp.routesCb(r);
179
+ const r = new index_js_2.default(this.expressApp);
180
+ await index_js_1.default.getOrFail().routesCb(r);
185
181
  r.commit();
186
182
  }
187
183
  }
@@ -0,0 +1,19 @@
1
+ import { inferSerializerFromDreamOrViewModel, isDreamSerializer, } from '@rvoh/dream';
2
+ export default function renderDreamOrVewModel(data, serializerKey,
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ passthrough, renderOpts) {
5
+ const serializer = inferSerializerFromDreamOrViewModel(data, serializerKey);
6
+ if (serializer && isDreamSerializer(serializer)) {
7
+ // passthrough data going into the serializer is the argument that gets
8
+ // used in the custom attribute callback function
9
+ return serializer(data, passthrough).render(
10
+ // passthrough data must be passed both into the serializer and render
11
+ // because, if the serializer does accept passthrough data, then passing it in is how
12
+ // it gets into the serializer, but if it does not accept passthrough data, and therefore
13
+ // does not pass it into the call to DreamSerializer/ObjectSerializer,
14
+ // then it would be lost to serializers rendered via rendersOne/Many, and SerializerRenderer
15
+ // handles passing its passthrough data into those
16
+ passthrough, renderOpts);
17
+ }
18
+ throw new Error(`${serializer?.constructor?.name} is not a Dream serializer`);
19
+ }
@@ -1,4 +1,4 @@
1
- import { Dream, DreamSerializerBuilder, GlobalNameNotSet, inferSerializerFromDreamOrViewModel, isDreamSerializer, ObjectSerializerBuilder, } from '@rvoh/dream';
1
+ import { Dream, DreamSerializerBuilder, GlobalNameNotSet, isDreamSerializer, ObjectSerializerBuilder, } from '@rvoh/dream';
2
2
  import ParamValidationError from '../error/controller/ParamValidationError.js';
3
3
  import HttpStatusBadGateway from '../error/http/BadGateway.js';
4
4
  import HttpStatusBadRequest from '../error/http/BadRequest.js';
@@ -31,10 +31,13 @@ import HttpStatusUnauthorized from '../error/http/Unauthorized.js';
31
31
  import HttpStatusUnavailableForLegalReasons from '../error/http/UnavailableForLegalReasons.js';
32
32
  import HttpStatusUnprocessableContent from '../error/http/UnprocessableContent.js';
33
33
  import HttpStatusUnsupportedMediaType from '../error/http/UnsupportedMediaType.js';
34
+ import toJson from '../helpers/toJson.js';
34
35
  import OpenapiPayloadValidator from '../openapi-renderer/helpers/OpenapiPayloadValidator.js';
36
+ import PsychicApp from '../psychic-app/index.js';
35
37
  import Params from '../server/params.js';
36
38
  import Session from '../session/index.js';
37
39
  import isPaginatedResult from './helpers/isPaginatedResult.js';
40
+ import renderDreamOrVewModel from './helpers/renderDreamOrViewModel.js';
38
41
  export const PsychicParamsPrimitiveLiterals = [
39
42
  'bigint',
40
43
  'bigint[]',
@@ -102,26 +105,6 @@ export default class PsychicController {
102
105
  */
103
106
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
107
  static openapi;
105
- /**
106
- * Enables you to specify specific serializers to use
107
- * when encountering specific models, i.e.
108
- *
109
- * ```ts
110
- * class MyController extends AuthedController {
111
- * static {
112
- * this.serializes(User).with(UserCustomSerializer)
113
- * }
114
- * }
115
- * ````
116
- */
117
- static serializes(ModelClass) {
118
- return {
119
- with: (SerializerClass) => {
120
- controllerSerializerIndex.add(this, SerializerClass, ModelClass);
121
- return this;
122
- },
123
- };
124
- }
125
108
  /**
126
109
  * @internal
127
110
  *
@@ -186,13 +169,11 @@ export default class PsychicController {
186
169
  req;
187
170
  res;
188
171
  session;
189
- config;
190
172
  action;
191
173
  renderOpts;
192
- constructor(req, res, { config, action, }) {
174
+ constructor(req, res, { action, }) {
193
175
  this.req = req;
194
176
  this.res = res;
195
- this.config = config;
196
177
  this.session = new Session(req, res);
197
178
  this.action = action;
198
179
  // TODO: read casing from Dream app config
@@ -351,13 +332,13 @@ export default class PsychicController {
351
332
  return this.session.setCookie(name, data, opts);
352
333
  }
353
334
  startSession(user) {
354
- return this.setCookie(this.config.sessionCookieName, JSON.stringify({
335
+ return this.setCookie(PsychicApp.getOrFail().sessionCookieName, JSON.stringify({
355
336
  id: user.primaryKeyValue().toString(),
356
337
  modelKey: user.constructor.globalName,
357
338
  }));
358
339
  }
359
340
  endSession() {
360
- return this.session.clearCookie(this.config.sessionCookieName);
341
+ return this.session.clearCookie(PsychicApp.getOrFail().sessionCookieName);
361
342
  }
362
343
  singleObjectJson(data, opts) {
363
344
  if (!data)
@@ -367,8 +348,6 @@ export default class PsychicController {
367
348
  if (data instanceof DreamSerializerBuilder || data instanceof ObjectSerializerBuilder) {
368
349
  return data.render(this.defaultSerializerPassthrough, this.renderOpts);
369
350
  }
370
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
371
- const lookup = controllerSerializerIndex.lookupModel(this.constructor, data.constructor);
372
351
  const openapiDef = this.constructor?.openapi?.[this.action];
373
352
  // passthrough data must be passed both into the serializer and render
374
353
  // because, if the serializer does accept passthrough data, then passing it in is how
@@ -377,48 +356,38 @@ export default class PsychicController {
377
356
  // then it would be lost to serializers rendered via rendersOne/Many, and SerializerRenderer
378
357
  // handles passing its passthrough data into those
379
358
  const passthrough = this.defaultSerializerPassthrough;
380
- if (lookup?.length) {
381
- const serializer = lookup?.[1];
382
- if (isDreamSerializer(serializer)) {
383
- // passthrough data going into the serializer is the argument that gets
384
- // used in the custom attribute callback function
359
+ if (data instanceof Dream || data.serializers) {
360
+ if (!opts.serializerKey && isDreamSerializer(openapiDef?.dreamsOrSerializers)) {
361
+ const serializer = openapiDef.dreamsOrSerializers;
385
362
  return serializer(data, passthrough).render(passthrough, this.renderOpts);
386
363
  }
387
- }
388
- else if (isDreamSerializer(openapiDef?.dreamsOrSerializers)) {
389
- const serializer = openapiDef.dreamsOrSerializers;
390
- return serializer(data, passthrough).render(passthrough, this.renderOpts);
391
- }
392
- else if (data instanceof Dream || data.serializers) {
393
- const serializer = inferSerializerFromDreamOrViewModel(data, opts.serializerKey ||
364
+ return renderDreamOrVewModel(data, opts.serializerKey ||
394
365
  psychicControllerClass['controllerActionMetadata'][this.action]?.['serializerKey'] ||
395
- 'default');
396
- if (serializer && isDreamSerializer(serializer)) {
397
- // passthrough data going into the serializer is the argument that gets
398
- // used in the custom attribute callback function
399
- return serializer(data, passthrough).render(
400
- // passthrough data must be passed both into the serializer and render
401
- // because, if the serializer does accept passthrough data, then passing it in is how
402
- // it gets into the serializer, but if it does not accept passthrough data, and therefore
403
- // does not pass it into the call to DreamSerializer/ObjectSerializer,
404
- // then it would be lost to serializers rendered via rendersOne/Many, and SerializerRenderer
405
- // handles passing its passthrough data into those
406
- passthrough, this.renderOpts);
407
- }
366
+ 'default', passthrough, this.renderOpts);
367
+ }
368
+ else {
369
+ return data;
408
370
  }
409
- return data;
410
371
  }
411
372
  json(data, opts = {}) {
412
- if (Array.isArray(data))
413
- return this.validateAndRenderJsonResponse(data.map(d =>
373
+ return this.validateAndRenderJsonResponse(this._json(data, opts));
374
+ }
375
+ _json(data, opts = {}) {
376
+ if (Array.isArray(data)) {
414
377
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
415
- this.singleObjectJson(d, opts)));
416
- if (isPaginatedResult(data))
417
- return this.validateAndRenderJsonResponse({
378
+ return data.map(d => this.singleObjectJson(d, opts));
379
+ //
380
+ }
381
+ else if (isPaginatedResult(data)) {
382
+ return {
418
383
  ...data,
419
384
  results: data.results.map(result => this.singleObjectJson(result, opts)),
420
- });
421
- return this.validateAndRenderJsonResponse(this.singleObjectJson(data, opts));
385
+ };
386
+ //
387
+ }
388
+ else {
389
+ return this.singleObjectJson(data, opts);
390
+ }
422
391
  }
423
392
  /**
424
393
  * Runs the data through openapi response validation, and then renders
@@ -430,7 +399,7 @@ export default class PsychicController {
430
399
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
431
400
  data) {
432
401
  this.validateOpenapiResponseBody(data);
433
- this.res.json(data);
402
+ this.res.type('json').send(toJson(data, PsychicApp.getOrFail().sanitizeResponseJson));
434
403
  }
435
404
  defaultSerializerPassthrough = {};
436
405
  serializerPassthrough(passthrough) {
@@ -788,13 +757,3 @@ export default class PsychicController {
788
757
  }
789
758
  }
790
759
  }
791
- export class ControllerSerializerIndex {
792
- associations = [];
793
- add(ControllerClass, SerializerClass, ModelClass) {
794
- this.associations.push([ControllerClass, SerializerClass, ModelClass]);
795
- }
796
- lookupModel(ControllerClass, ModelClass) {
797
- return this.associations.find(association => association[0] === ControllerClass && association[2] === ModelClass);
798
- }
799
- }
800
- export const controllerSerializerIndex = new ControllerSerializerIndex();
@@ -0,0 +1,25 @@
1
+ const CHARACTERS_TO_SANITIZE_REGEXP = /[\\&<>/'"]/g;
2
+ export default function sanitizeString(str) {
3
+ if (str === null || str === undefined)
4
+ return str;
5
+ return str.replace(CHARACTERS_TO_SANITIZE_REGEXP, function (char) {
6
+ switch (char) {
7
+ case '\\':
8
+ return '\\u005c';
9
+ case '/':
10
+ return '\\u002f';
11
+ case '<':
12
+ return '\\u003c';
13
+ case '>':
14
+ return '\\u003e';
15
+ case '&':
16
+ return '\\u0026';
17
+ case "'":
18
+ return '\\u0027';
19
+ case '"':
20
+ return '\\u0022';
21
+ default:
22
+ return char;
23
+ }
24
+ });
25
+ }
@@ -0,0 +1,15 @@
1
+ import sanitizeString from './sanitizeString.js';
2
+ export default function toJson(data, sanitize) {
3
+ /**
4
+ * 'undefined' is invalid json, and `JSON.stringify(undefined)` returns `undefined`
5
+ * so follow the pattern established by Express and return '{}' for `undefined`
6
+ */
7
+ if (data === undefined)
8
+ return '{}';
9
+ if (sanitize) {
10
+ return JSON.stringify(data, (_, x) => (typeof x !== 'string' ? x : sanitizeString(x))).replace(/\\\\/g, '\\');
11
+ }
12
+ else {
13
+ return JSON.stringify(data);
14
+ }
15
+ }
@@ -41,6 +41,9 @@ export { default as generateController } from './generate/controller.js';
41
41
  export { default as generateResource } from './generate/resource.js';
42
42
  export { default as cookieMaxAgeFromCookieOpts } from './helpers/cookieMaxAgeFromCookieOpts.js';
43
43
  export { default as pathifyNestedObject } from './helpers/pathifyNestedObject.js';
44
+ export { default as sanitizeString } from './helpers/sanitizeString.js';
45
+ export { default as ParamValidationError } from './error/controller/ParamValidationError.js';
46
+ export { default as ParamValidationErrors } from './error/controller/ParamValidationErrors.js';
44
47
  export { MissingControllerActionPairingInRoutes, } from './openapi-renderer/endpoint.js';
45
48
  export { default as PsychicImporter } from './psychic-app/helpers/PsychicImporter.js';
46
49
  export { default as PsychicApp, } from './psychic-app/index.js';
@@ -48,6 +51,4 @@ export { default as PsychicRouter } from './router/index.js';
48
51
  export { createPsychicHttpInstance as getPsychicHttpInstance } from './server/helpers/startPsychicServer.js';
49
52
  export { default as PsychicServer } from './server/index.js';
50
53
  export { default as Params } from './server/params.js';
51
- export { default as ParamValidationError } from './error/controller/ParamValidationError.js';
52
- export { default as ParamValidationErrors } from './error/controller/ParamValidationErrors.js';
53
54
  export { default as PsychicSession } from './session/index.js';
@@ -32,10 +32,7 @@ importCb) {
32
32
  * at decoration time such that the class of a property being decorated is only avilable during instance instantiation. In order
33
33
  * to only apply static values once, on boot, `globallyInitializingDecorators` is set to true on Dream, and all Dream models are instantiated.
34
34
  */
35
- new controllerClass({}, {}, {
36
- action: 'a',
37
- config: psychicApp,
38
- });
35
+ new controllerClass({}, {}, { action: 'a' });
39
36
  }
40
37
  }
41
38
  PsychicController['globallyInitializingDecorators'] = false;
@@ -82,7 +82,7 @@ export default class PsychicApp {
82
82
  async buildRoutesCache() {
83
83
  if (this._routesCache)
84
84
  return;
85
- const r = new PsychicRouter(null, this);
85
+ const r = new PsychicRouter(null);
86
86
  await this.routesCb(r);
87
87
  this._routesCache = r.routes;
88
88
  }
@@ -244,6 +244,10 @@ Try setting it to something valid, like:
244
244
  get saltRounds() {
245
245
  return this._saltRounds;
246
246
  }
247
+ _sanitizeResponseJson = false;
248
+ get sanitizeResponseJson() {
249
+ return this._sanitizeResponseJson;
250
+ }
247
251
  _packageManager;
248
252
  get packageManager() {
249
253
  return this._packageManager;
@@ -535,6 +539,9 @@ Try setting it to something valid, like:
535
539
  case 'saltRounds':
536
540
  this._saltRounds = value;
537
541
  break;
542
+ case 'sanitizeResponseJson':
543
+ this._sanitizeResponseJson = value;
544
+ break;
538
545
  case 'openapi':
539
546
  this._openapi = {
540
547
  ...this.openapi,
@@ -13,15 +13,10 @@ import RouteManager from './route-manager.js';
13
13
  import { ResourceMethods, ResourcesMethods, } from './types.js';
14
14
  export default class PsychicRouter {
15
15
  app;
16
- config;
17
16
  currentNamespaces = [];
18
17
  routeManager = new RouteManager();
19
- constructor(app, config) {
18
+ constructor(app) {
20
19
  this.app = app;
21
- this.config = config;
22
- }
23
- get routingMechanism() {
24
- return this.app;
25
20
  }
26
21
  get routes() {
27
22
  return this.routeManager.routes;
@@ -109,13 +104,13 @@ suggested fix: "${convertRouteParams(path)}"
109
104
  `);
110
105
  }
111
106
  namespace(namespace, cb) {
112
- const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
107
+ const nestedRouter = new PsychicNestedRouter(this.app, this.routeManager, {
113
108
  namespaces: this.currentNamespaces,
114
109
  });
115
110
  this.runNestedCallbacks(namespace, nestedRouter, cb);
116
111
  }
117
112
  scope(scope, 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(scope, nestedRouter, cb, { treatNamespaceAsScope: true });
@@ -128,7 +123,7 @@ suggested fix: "${convertRouteParams(path)}"
128
123
  }
129
124
  collection(cb) {
130
125
  const replacedNamespaces = this.currentNamespaces.slice(0, this.currentNamespaces.length - 1);
131
- const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
126
+ const nestedRouter = new PsychicNestedRouter(this.app, this.routeManager, {
132
127
  namespaces: replacedNamespaces,
133
128
  });
134
129
  const currentNamespace = replacedNamespaces[replacedNamespaces.length - 1];
@@ -150,7 +145,7 @@ suggested fix: "${convertRouteParams(path)}"
150
145
  }
151
146
  }
152
147
  _makeResource(path, options, cb, plural) {
153
- const nestedRouter = new PsychicNestedRouter(this.app, this.config, this.routeManager, {
148
+ const nestedRouter = new PsychicNestedRouter(this.app, this.routeManager, {
154
149
  namespaces: this.currentNamespaces,
155
150
  });
156
151
  const { only, except } = options || {};
@@ -283,9 +278,9 @@ suggested fix: "${convertRouteParams(path)}"
283
278
  PsychicApp.log('ATTENTION: a server error was detected:');
284
279
  PsychicApp.logWithLevel('error', err);
285
280
  }
286
- if (this.config.specialHooks.serverError.length) {
281
+ if (PsychicApp.getOrFail().specialHooks.serverError.length) {
287
282
  try {
288
- for (const hook of this.config.specialHooks.serverError) {
283
+ for (const hook of PsychicApp.getOrFail().specialHooks.serverError) {
289
284
  await hook(err, req, res);
290
285
  }
291
286
  }
@@ -315,15 +310,14 @@ suggested fix: "${convertRouteParams(path)}"
315
310
  }
316
311
  _initializeController(ControllerClass, req, res, action) {
317
312
  return new ControllerClass(req, res, {
318
- config: this.config,
319
313
  action,
320
314
  });
321
315
  }
322
316
  }
323
317
  export class PsychicNestedRouter extends PsychicRouter {
324
318
  router;
325
- constructor(expressApp, config, routeManager, { namespaces = [], } = {}) {
326
- super(expressApp, config);
319
+ constructor(expressApp, routeManager, { namespaces = [], } = {}) {
320
+ super(expressApp);
327
321
  this.router = Router();
328
322
  this.currentNamespaces = namespaces;
329
323
  this.routeManager = routeManager;
@@ -2,10 +2,10 @@ import { closeAllDbConnections, DreamLogos } from '@rvoh/dream';
2
2
  import * as cookieParser from 'cookie-parser';
3
3
  import * as cors from 'cors';
4
4
  import * as express from 'express';
5
+ import EnvInternal from '../helpers/EnvInternal.js';
5
6
  import PsychicApp from '../psychic-app/index.js';
6
7
  import PsychicRouter from '../router/index.js';
7
8
  import startPsychicServer, { createPsychicHttpInstance, } from './helpers/startPsychicServer.js';
8
- import EnvInternal from '../helpers/EnvInternal.js';
9
9
  // const debugEnabled = debuglog('psychic').enabled
10
10
  export default class PsychicServer {
11
11
  static async startPsychicServer(opts) {
@@ -23,13 +23,9 @@ export default class PsychicServer {
23
23
  constructor() {
24
24
  this.buildApp();
25
25
  }
26
- get config() {
27
- return PsychicApp.getOrFail();
28
- }
29
26
  async routes() {
30
- const r = new PsychicRouter(this.expressApp, this.config);
31
- const psychicApp = PsychicApp.getOrFail();
32
- await psychicApp.routesCb(r);
27
+ const r = new PsychicRouter(this.expressApp);
28
+ await PsychicApp.getOrFail().routesCb(r);
33
29
  return r.routes;
34
30
  }
35
31
  async boot() {
@@ -43,13 +39,14 @@ export default class PsychicServer {
43
39
  });
44
40
  next();
45
41
  });
46
- for (const serverInitBeforeMiddlewareHook of this.config.specialHooks.serverInitBeforeMiddleware) {
42
+ for (const serverInitBeforeMiddlewareHook of PsychicApp.getOrFail().specialHooks
43
+ .serverInitBeforeMiddleware) {
47
44
  await serverInitBeforeMiddlewareHook(this);
48
45
  }
49
46
  this.initializeCors();
50
47
  this.initializeJSON();
51
48
  try {
52
- await this.config.boot();
49
+ await PsychicApp.getOrFail().boot();
53
50
  }
54
51
  catch (err) {
55
52
  const error = err;
@@ -59,11 +56,12 @@ export default class PsychicServer {
59
56
  ${error.message}
60
57
  `);
61
58
  }
62
- for (const serverInitAfterMiddlewareHook of this.config.specialHooks.serverInitAfterMiddleware) {
59
+ for (const serverInitAfterMiddlewareHook of PsychicApp.getOrFail().specialHooks
60
+ .serverInitAfterMiddleware) {
63
61
  await serverInitAfterMiddlewareHook(this);
64
62
  }
65
63
  await this.buildRoutes();
66
- for (const afterRoutesHook of this.config.specialHooks.serverInitAfterRoutes) {
64
+ for (const afterRoutesHook of PsychicApp.getOrFail().specialHooks.serverInitAfterRoutes) {
67
65
  await afterRoutesHook(this);
68
66
  }
69
67
  this.booted = true;
@@ -91,7 +89,7 @@ export default class PsychicServer {
91
89
  const httpServer = await startPsychicServer({
92
90
  app: this.expressApp,
93
91
  port: port || psychicApp.port,
94
- sslCredentials: this.config.sslCredentials,
92
+ sslCredentials: PsychicApp.getOrFail().sslCredentials,
95
93
  });
96
94
  this.httpServer = httpServer;
97
95
  }
@@ -118,8 +116,7 @@ export default class PsychicServer {
118
116
  process.exit();
119
117
  }
120
118
  async stop({ bypassClosingDbConnections = false } = {}) {
121
- const psychicApp = PsychicApp.getOrFail();
122
- for (const hook of psychicApp.specialHooks.serverShutdown) {
119
+ for (const hook of PsychicApp.getOrFail().specialHooks.serverShutdown) {
123
120
  await hook(this);
124
121
  }
125
122
  this.httpServer?.close();
@@ -128,8 +125,7 @@ export default class PsychicServer {
128
125
  }
129
126
  }
130
127
  async serveForRequestSpecs(block) {
131
- const psychicApp = PsychicApp.getOrFail();
132
- const port = psychicApp.port;
128
+ const port = PsychicApp.getOrFail().port;
133
129
  await this.boot();
134
130
  let server;
135
131
  await new Promise(accept => {
@@ -144,16 +140,16 @@ export default class PsychicServer {
144
140
  this.expressApp.use(cookieParser.default());
145
141
  }
146
142
  initializeCors() {
143
+ this.expressApp.use(
147
144
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
- this.expressApp.use(cors.default(this.config.corsOptions));
145
+ cors.default(PsychicApp.getOrFail().corsOptions));
149
146
  }
150
147
  initializeJSON() {
151
- this.expressApp.use(express.json(this.config.jsonOptions));
148
+ this.expressApp.use(express.json(PsychicApp.getOrFail().jsonOptions));
152
149
  }
153
150
  async buildRoutes() {
154
- const r = new PsychicRouter(this.expressApp, this.config);
155
- const psychicApp = PsychicApp.getOrFail();
156
- await psychicApp.routesCb(r);
151
+ const r = new PsychicRouter(this.expressApp);
152
+ await PsychicApp.getOrFail().routesCb(r);
157
153
  r.commit();
158
154
  }
159
155
  }
@@ -0,0 +1,2 @@
1
+ import { Dream, SerializerRendererOpts, ViewModel } from '@rvoh/dream';
2
+ export default function renderDreamOrVewModel(data: Dream | ViewModel, serializerKey: string, passthrough: any, renderOpts: SerializerRendererOpts): Record<string, any> | null;
@@ -1,9 +1,8 @@
1
- import { Dream, DreamModelSerializerType, DreamParamSafeAttributes, DreamParamSafeColumnNames, OpenapiSchemaBody, SerializerRendererOpts, SimpleObjectSerializerType, UpdateableProperties } from '@rvoh/dream';
1
+ import { Dream, DreamParamSafeAttributes, DreamParamSafeColumnNames, OpenapiSchemaBody, SerializerRendererOpts, UpdateableProperties } from '@rvoh/dream';
2
2
  import { Request, Response } from 'express';
3
3
  import { ControllerHook } from '../controller/hooks.js';
4
4
  import { HttpStatusCodeInt, HttpStatusSymbol } from '../error/http/status-codes.js';
5
5
  import OpenapiEndpointRenderer from '../openapi-renderer/endpoint.js';
6
- import PsychicApp from '../psychic-app/index.js';
7
6
  import { ParamsCastOptions, ParamsForOpts, ValidatedAllowsNull, ValidatedReturnType } from '../server/params.js';
8
7
  import Session, { CustomSessionCookieOptions } from '../session/index.js';
9
8
  type SerializerResult = {
@@ -58,21 +57,6 @@ export default class PsychicController {
58
57
  * by using the `@Openapi` decorator on your controller methods
59
58
  */
60
59
  static openapi: Record<string, OpenapiEndpointRenderer<any, any>>;
61
- /**
62
- * Enables you to specify specific serializers to use
63
- * when encountering specific models, i.e.
64
- *
65
- * ```ts
66
- * class MyController extends AuthedController {
67
- * static {
68
- * this.serializes(User).with(UserCustomSerializer)
69
- * }
70
- * }
71
- * ````
72
- */
73
- static serializes(ModelClass: typeof Dream): {
74
- with: (SerializerClass: DreamModelSerializerType | SimpleObjectSerializerType) => typeof PsychicController;
75
- };
76
60
  /**
77
61
  * @internal
78
62
  *
@@ -118,11 +102,9 @@ export default class PsychicController {
118
102
  req: Request;
119
103
  res: Response;
120
104
  session: Session;
121
- config: PsychicApp;
122
105
  action: string;
123
106
  renderOpts: SerializerRendererOpts;
124
- constructor(req: Request, res: Response, { config, action, }: {
125
- config: PsychicApp;
107
+ constructor(req: Request, res: Response, { action, }: {
126
108
  action: string;
127
109
  });
128
110
  /**
@@ -242,6 +224,7 @@ export default class PsychicController {
242
224
  endSession(): void;
243
225
  private singleObjectJson;
244
226
  json<T>(data: T, opts?: RenderOptions): any;
227
+ private _json;
245
228
  /**
246
229
  * Runs the data through openapi response validation, and then renders
247
230
  * the data if no errors were found.
@@ -359,16 +342,6 @@ export default class PsychicController {
359
342
  */
360
343
  runBeforeActionsFor(action: string): Promise<void>;
361
344
  }
362
- export declare class ControllerSerializerIndex {
363
- associations: [
364
- typeof PsychicController,
365
- DreamModelSerializerType | SimpleObjectSerializerType,
366
- typeof Dream
367
- ][];
368
- add(ControllerClass: typeof PsychicController, SerializerClass: DreamModelSerializerType | SimpleObjectSerializerType, ModelClass: typeof Dream): void;
369
- lookupModel(ControllerClass: typeof PsychicController, ModelClass: typeof Dream): [typeof PsychicController, DreamModelSerializerType | SimpleObjectSerializerType, typeof Dream] | undefined;
370
- }
371
- export declare const controllerSerializerIndex: ControllerSerializerIndex;
372
345
  export type RenderOptions = {
373
346
  serializerKey?: string;
374
347
  };
@@ -0,0 +1 @@
1
+ export default function sanitizeString<T extends string | null | undefined>(str: T): T;
@@ -0,0 +1 @@
1
+ export default function toJson<T>(data: T, sanitize: boolean): string;
@@ -50,6 +50,9 @@ export { default as generateController } from './generate/controller.js';
50
50
  export { default as generateResource } from './generate/resource.js';
51
51
  export { default as cookieMaxAgeFromCookieOpts } from './helpers/cookieMaxAgeFromCookieOpts.js';
52
52
  export { default as pathifyNestedObject } from './helpers/pathifyNestedObject.js';
53
+ export { default as sanitizeString } from './helpers/sanitizeString.js';
54
+ export { default as ParamValidationError } from './error/controller/ParamValidationError.js';
55
+ export { default as ParamValidationErrors } from './error/controller/ParamValidationErrors.js';
53
56
  export { MissingControllerActionPairingInRoutes, type OpenapiContent, type OpenapiEndpointRendererOpts, type OpenapiEndpointResponse, type OpenapiHeaderOption, type OpenapiHeaders, type OpenapiHeaderType, type OpenapiMethodBody, type OpenapiParameterResponse, type OpenapiPathParams, type OpenapiQueryOption, type OpenapiResponses, type OpenapiSchema, type OpenapiPathParamOption as OpenapiUriOption, } from './openapi-renderer/endpoint.js';
54
57
  export { default as PsychicImporter } from './psychic-app/helpers/PsychicImporter.js';
55
58
  export { default as PsychicApp, type DefaultPsychicOpenapiOptions, type NamedPsychicOpenapiOptions, type PsychicAppInitOptions, } from './psychic-app/index.js';
@@ -59,6 +62,4 @@ export { type HttpMethod } from './router/types.js';
59
62
  export { createPsychicHttpInstance as getPsychicHttpInstance } from './server/helpers/startPsychicServer.js';
60
63
  export { default as PsychicServer } from './server/index.js';
61
64
  export { default as Params } from './server/params.js';
62
- export { default as ParamValidationError } from './error/controller/ParamValidationError.js';
63
- export { default as ParamValidationErrors } from './error/controller/ParamValidationErrors.js';
64
65
  export { default as PsychicSession } from './session/index.js';
@@ -127,6 +127,8 @@ export default class PsychicApp {
127
127
  get sslCredentials(): PsychicSslCredentials | undefined;
128
128
  private _saltRounds;
129
129
  get saltRounds(): number | undefined;
130
+ private _sanitizeResponseJson;
131
+ get sanitizeResponseJson(): boolean;
130
132
  private _packageManager;
131
133
  get packageManager(): "yarn" | "npm" | "pnpm";
132
134
  private _importExtension;
@@ -219,10 +221,10 @@ export default class PsychicApp {
219
221
  plugin(cb: (app: PsychicApp) => void | Promise<void>): void;
220
222
  on<T extends PsychicHookEventType>(hookEventType: T, cb: T extends 'server:error' ? (err: Error, req: Request, res: Response) => void | Promise<void> : T extends 'server:init:before-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:start' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:shutdown' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-routes' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'cli:start' ? (program: Command) => void | Promise<void> : T extends 'cli:sync' ? () => any : (conf: PsychicApp) => void | Promise<void>): void;
221
223
  set(option: 'openapi', name: string, value: NamedPsychicOpenapiOptions): void;
222
- set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? CorsOptions : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'importExtension' ? GeneratorImportStyle : Opt extends 'sessionCookieName' ? string : Opt extends 'clientRoot' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
224
+ set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? CorsOptions : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'importExtension' ? GeneratorImportStyle : Opt extends 'sessionCookieName' ? string : Opt extends 'clientRoot' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'sanitizeResponseJson' ? boolean : Opt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
223
225
  override<Override extends keyof PsychicAppOverrides>(override: Override, value: PsychicAppOverrides[Override]): void;
224
226
  }
225
- export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'importExtension' | 'encryption' | 'sessionCookieName' | 'clientRoot' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'ssl';
227
+ export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'importExtension' | 'encryption' | 'sessionCookieName' | 'clientRoot' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'sanitizeResponseJson' | 'ssl';
226
228
  export interface PsychicAppSpecialHooks {
227
229
  cliSync: (() => any)[];
228
230
  serverInitBeforeMiddleware: ((server: PsychicServer) => void | Promise<void>)[];
@@ -1,16 +1,13 @@
1
1
  import { Application, Request, RequestHandler, Response, Router } from 'express';
2
2
  import PsychicController from '../controller/index.js';
3
- import PsychicApp from '../psychic-app/index.js';
4
3
  import { NamespaceConfig, PsychicControllerActions } from '../router/helpers.js';
5
4
  import RouteManager from './route-manager.js';
6
5
  import { HttpMethod, ResourcesOptions } from './types.js';
7
6
  export default class PsychicRouter {
8
7
  app: Application | null;
9
- config: PsychicApp;
10
8
  currentNamespaces: NamespaceConfig[];
11
9
  routeManager: RouteManager;
12
- constructor(app: Application | null, config: PsychicApp);
13
- get routingMechanism(): Application | Router | null;
10
+ constructor(app: Application | null);
14
11
  get routes(): import("./route-manager.js").RouteConfig[];
15
12
  private get currentNamespacePaths();
16
13
  commit(): void;
@@ -56,7 +53,7 @@ export default class PsychicRouter {
56
53
  }
57
54
  export declare class PsychicNestedRouter extends PsychicRouter {
58
55
  router: Router;
59
- constructor(expressApp: Application | null, config: PsychicApp, routeManager: RouteManager, { namespaces, }?: {
56
+ constructor(expressApp: Application | null, routeManager: RouteManager, { namespaces, }?: {
60
57
  namespaces?: NamespaceConfig[];
61
58
  });
62
59
  }
@@ -1,6 +1,6 @@
1
1
  import { Express } from 'express';
2
2
  import { Server } from 'node:http';
3
- import PsychicApp, { PsychicSslCredentials } from '../psychic-app/index.js';
3
+ import { PsychicSslCredentials } from '../psychic-app/index.js';
4
4
  import { StartPsychicServerOptions } from './helpers/startPsychicServer.js';
5
5
  export default class PsychicServer {
6
6
  static startPsychicServer(opts: StartPsychicServerOptions): Promise<Server>;
@@ -10,7 +10,6 @@ export default class PsychicServer {
10
10
  httpServer: Server;
11
11
  private booted;
12
12
  constructor();
13
- get config(): PsychicApp;
14
13
  routes(): Promise<import("../router/route-manager.js").RouteConfig[]>;
15
14
  boot(): Promise<true | undefined>;
16
15
  private setSecureDefaultHeaders;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic",
4
4
  "description": "Typescript web framework",
5
- "version": "1.6.4",
5
+ "version": "1.7.0",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -62,7 +62,7 @@
62
62
  "devDependencies": {
63
63
  "@eslint/js": "^9.19.0",
64
64
  "@jest-mock/express": "^3.0.0",
65
- "@rvoh/dream": "^1.5.2",
65
+ "@rvoh/dream": "^1.7.0",
66
66
  "@rvoh/dream-spec-helpers": "^1.1.1",
67
67
  "@rvoh/psychic-spec-helpers": "^1.0.0",
68
68
  "@types/express": "^5.0.1",