@rvoh/psychic 1.8.0 → 1.8.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,262 @@
1
+ ## 1.8.1
2
+
3
+ - do not coerce types in ajv when processing request or response bodies during validation. Type coercion will still happen for headers and query params, since they will need to respect the schema type specified in the openapi docuement.
4
+
5
+ ## 1.8.0
6
+
7
+ - remove unused `clientRoot` config
8
+
9
+ ## 1.7.2
10
+
11
+ - generate admin routes in routes.admin.ts (requires `routes.admin.ts` next to `routes.ts`)
12
+
13
+ ## 1.7.1
14
+
15
+ - compute openapi doc during intiialization, rather than problematically reading from a file cache
16
+
17
+ ## 1.7.0
18
+
19
+ - `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")
20
+
21
+ - Fix openapi serializer fallback issue introduced in 1.6.3, where we mistakenly double render data that has already been serialized.
22
+
23
+ ## 1.6.4
24
+
25
+ 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.
26
+
27
+ ## 1.6.3
28
+
29
+ - castParam accepts raw openapi shapes as type arguments, correctly casting the result to an interface representing the provided openapi shape.
30
+
31
+ ```ts
32
+ class MyController extends ApplicationController {
33
+ public index() {
34
+ const myParam = this.castParam('myParam', {
35
+ type: 'array',
36
+ items: {
37
+ anyOf: [{ type: 'string' }, { type: 'number' }],
38
+ },
39
+ })
40
+ myParam[0] // string | number
41
+ }
42
+ }
43
+ ```
44
+
45
+ - simplify the needlessly-robust new psychic router patterns by making expressApp optional, essentially reverting us back to the same psychic router we had prior to the recent openapi validation changes.
46
+
47
+ - fallback to serializer specified in openapi decorator before falling back to dream serializer when rendering dreams
48
+
49
+ ## 1.6.2
50
+
51
+ fix OpenAPI spec generation by DRYing up generation of request and response body
52
+
53
+ ## 1.6.1
54
+
55
+ fix issue preventing validation fallbacks from properly overriding on OpenAPI decorator calls when explicitly opting out of validation
56
+
57
+ ## 1.6.0
58
+
59
+ enables validation to be added to both openapi configurations, as well as to `OpenAPI` decorator calls, enabling the developer to granularly control validation logic for their endpoints.
60
+
61
+ To leverage global config:
62
+
63
+ ```ts
64
+ // conf/app.ts
65
+ export default async (psy: PsychicApp) => {
66
+ ...
67
+
68
+ psy.set('openapi', {
69
+ // ...
70
+ validate: {
71
+ headers: true,
72
+ requestBody: true,
73
+ query: true,
74
+ responseBody: AppEnv.isTest,
75
+ },
76
+ })
77
+ }
78
+ ```
79
+
80
+ To leverage endpoint config:
81
+
82
+ ```ts
83
+ // controllers/PetsController
84
+ export default class PetsController {
85
+ @OpenAPI(Pet, {
86
+ ...
87
+ validate: {
88
+ headers: true,
89
+ requestBody: true,
90
+ query: true,
91
+ responseBody: AppEnv.isTest,
92
+ }
93
+ })
94
+ public async index() {
95
+ ...
96
+ }
97
+ }
98
+ ```
99
+
100
+ This PR additionally formally introduces a new possible error type for 400 status codes, and to help distinguish, it also introduces a `type` field, which can be either `openapi` or `dream` to aid the developer in easily handling the various cases.
101
+
102
+ We have made a conscious decision to render openapi errors in the exact format that ajv returns, since it empowers the developer to utilize tools which can already respond to ajv errors.
103
+
104
+ For added flexibility, this PR includes the ability to provide configuration overrides for the ajv instance, as well as the ability to provide an initialization function to override ajv behavior, since much of the configuration for ajv is driven by method calls, rather than simple config.
105
+
106
+ ```ts
107
+ // controllers/PetsController
108
+ export default class PetsController {
109
+ @OpenAPI(Pet, {
110
+ ...
111
+ validate: {
112
+ ajvOptions: {
113
+ // this is off by default, but you will
114
+ // always want to keep this off in prod
115
+ // to avoid DoS vulnerabilities
116
+ allErrors: AppEnv.isTest,
117
+
118
+ // provide a custom init function to further
119
+ // configure your ajv instance before validating
120
+ init: ajv => {
121
+ ajv.addFormat('myFormat', {
122
+ type: 'string',
123
+ validate: data => MY_FORMAT_REGEX.test(data),
124
+ })
125
+ }
126
+ }
127
+ }
128
+ })
129
+ public async index() {
130
+ ...
131
+ }
132
+ }
133
+ ```
134
+
135
+ ## 1.5.5
136
+
137
+ - ensure that openapi-typescript and typescript are not required dependencies when running migrations with --skip-sync flag
138
+
139
+ ## 1.5.4
140
+
141
+ - fix issue when providing the `including` argument exclusively to an OpenAPI decorator's `requestBody`
142
+
143
+ ## 1.5.3
144
+
145
+ - add missing peer dependency for openapi-typescript, allow BIGINT type when generating openapi-typescript bigints
146
+
147
+ ## 1.5.2
148
+
149
+ - ensure that bigints are converted to number | string when generating openapi-typescript type files
150
+
151
+ ## 1.5.1
152
+
153
+ - fix issue with enum syncing related to multi-db engine support regression
154
+
155
+ ## 1.5.0
156
+
157
+ - add support for multiple database engines in dream
158
+
159
+ ## 1.2.3
160
+
161
+ - add support for the connectionName argument when generating a resource
162
+
163
+ ## 1.2.2
164
+
165
+ - bump supertest and express-session to close dependabot issues [53](https://github.com/rvohealth/psychic/security/dependabot/53), [56](https://github.com/rvohealth/psychic/security/dependabot/56), and [57](https://github.com/rvohealth/psychic/security/dependabot/57)
166
+
167
+ ## 1.2.1
168
+
169
+ - add ability to set custom import extension, which will be used when generating new files for your application
170
+
171
+ ## 1.2.0
172
+
173
+ - update for Dream 1.4.0
174
+
175
+ ## 1.1.11
176
+
177
+ - 400 is more appropriate than 422 for `DataTypeColumnTypeMismatch`
178
+
179
+ ## 1.1.10
180
+
181
+ - Don't include deletedAt in generated create/update actions in resource specs since deletedAt is for deleting
182
+
183
+ - return 422 if Dream throws `NotNullViolation` or `CheckConstraintViolation`
184
+
185
+ ## 1.1.9
186
+
187
+ - return 422 if dream throws `DataTypeColumnTypeMismatch`, which happens when a dream is saved to the database with data that cannot be inserted into the respective columns, usually because of a type mismatch.
188
+
189
+ - castParam will now encase params in an array when being explicitly casted as an array type, bypassing a known bug in express from causing arrays with single items in them to be treated as non-arrays.
190
+
191
+ ## 1.1.8
192
+
193
+ - Tap into CliFileWriter provided by dream to tap into file reversion for sync files, since the auto-sync function in psychic can fail and leave your file tree in a bad state.
194
+
195
+ ## 1.1.7
196
+
197
+ - Add support for middleware arrays, enabling express plugins like passport
198
+
199
+ ## 1.1.6
200
+
201
+ - fix regression caused by missing --schema-only option in psychic cli
202
+
203
+ ## 1.1.5
204
+
205
+ - pass packageManager through to dream, now that it accepts a packageManager setting.
206
+ - update dream shadowing within psychic application initialization to take place after initializers and plugins are processed, so that those initializers and plugins have an opportunity to adjust the settings.
207
+
208
+ ## 1.1.4
209
+
210
+ - fix regressions to redux bindings caused by default openapi path location changes
211
+ - resource generator can handle prefixing slashes
212
+
213
+ ## 1.1.3
214
+
215
+ - fix more minor issues with redux openapi bindings
216
+
217
+ ## 1.1.2
218
+
219
+ - Fix various issues with openapi redux bindings
220
+ - raise hard exception if accidentally using openapi route params in an expressjs route path
221
+
222
+ ## 1.1.1
223
+
224
+ Fix route printing regression causing route printouts to show the path instead of the action
225
+
226
+ ## v1.1.0
227
+
228
+ Provides easier access to express middleware by exposing `PsychicApp#use`, which enables a developer to provide express middleware directly through the psychcic application, without tapping into any hooks.
229
+
230
+ ```ts
231
+ psy.use((_, res) => {
232
+ res.send(
233
+ 'this will be run after psychic middleware (i.e. cors and bodyParser) are processed, but before routes are processed',
234
+ )
235
+ })
236
+ ```
237
+
238
+ Some middleware needs to be run before other middleware, so we expose an optional first argument which can be provided so explicitly send your middleware into express at various stages of the psychic configuration process. For example, to inject your middleware before cors and bodyParser are configured, provide `before-middleware` as the first argument. To initialize your middleware after the psychic default middleware, but before your routes have been processed, provide `after-middleware` as the first argument (or simply provide a callback function directly, since this is the default). To run after routes have been processed, provide `after-routes` as the first argument.
239
+
240
+ ```ts
241
+ psy.use('before-middleware', (_, res) => {
242
+ res.send('this will be run before psychic has configured any default middleware')
243
+ })
244
+
245
+ psy.use('after-middleware', (_, res) => {
246
+ res.send('this will be run after psychic has configured default middleware')
247
+ })
248
+
249
+ psy.use('after-routes', (_, res) => {
250
+ res.send('this will be run after psychic has processed all the routes in your conf/routes.ts file')
251
+ })
252
+ ```
253
+
254
+ Additionally, a new overload has been added to all CRUD methods on the PsychicRouter class, enabling you to provide RequestHandler middleware directly to psychic, like so:
255
+
256
+ ```ts
257
+ // conf/routes.ts
258
+
259
+ r.get('helloworld', (req, res, next) => {
260
+ res.json({ hello: 'world' })
261
+ })
262
+ ```
@@ -46,8 +46,6 @@ const ajv_formats_1 = __importDefault(require("ajv-formats"));
46
46
  function validateOpenApiSchema(data, openapiSchema, options = {}) {
47
47
  return validateObject(data, openapiSchema, {
48
48
  removeAdditional: 'failing', // Remove properties that fail validation
49
- useDefaults: true,
50
- coerceTypes: true,
51
49
  ...options,
52
50
  });
53
51
  }
@@ -112,9 +110,10 @@ function createValidator(schema, options = {}) {
112
110
  const ajv = new ajv_1.Ajv({
113
111
  removeAdditional: false,
114
112
  useDefaults: true,
115
- coerceTypes: true,
116
- strict: false, // Allow unknown keywords for OpenAPI compatibility
117
- allErrors: options.allErrors || false, // Collect all errors, not just the first one
113
+ strict: false,
114
+ coerceTypes: false,
115
+ strictTypes: true,
116
+ allErrors: false,
118
117
  validateFormats: true, // Enable format validation for date-time, email, etc.
119
118
  ...ajvOptions,
120
119
  });
@@ -209,7 +209,7 @@ class OpenapiPayloadValidator {
209
209
  validateOrFail(
210
210
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
211
211
  data, openapiSchema, target) {
212
- const validationResults = (0, validateOpenApiSchema_js_1.default)(data || {}, this.addComponentsToSchema(openapiSchema), this.getAjvOptions());
212
+ const validationResults = (0, validateOpenApiSchema_js_1.default)(data || {}, this.addComponentsToSchema(openapiSchema), this.getAjvOptions(target));
213
213
  if (!validationResults.isValid) {
214
214
  const errorClass = target === 'responseBody' ? OpenapResponseValidationFailure_js_1.default : OpenapiRequestValidationFailure_js_1.default;
215
215
  throw new errorClass(validationResults.errors || [], target);
@@ -242,12 +242,17 @@ class OpenapiPayloadValidator {
242
242
  * which is automatically handled by psychic and turned into a 400
243
243
  * with the respective errors attached.
244
244
  */
245
- getAjvOptions() {
245
+ getAjvOptions(target) {
246
246
  const rendererOpts = this.openapiEndpointRenderer['validate']?.['ajvOptions'];
247
247
  const openapiConf = index_js_1.default.getOrFail().openapi?.[this.openapiName];
248
248
  return {
249
249
  ...openapiConf?.validate?.ajvOptions,
250
250
  ...rendererOpts,
251
+ // if headers or query, we want to levarage ajv type coercion,
252
+ // so that a query or header parameter can have a type of i.e. number
253
+ // or boolean without failing to coerce, since by default our
254
+ // ajv instance will be instantiated with coerceTypes=false
255
+ coerceTypes: target === 'headers' || target === 'query',
251
256
  };
252
257
  }
253
258
  }
@@ -299,7 +299,7 @@ class Params {
299
299
  return paramValue.map(param => this.cast(paramName, param, arrayTypeToNonArrayType(expectedType), { ...opts, allowNull: true }));
300
300
  default:
301
301
  if (this.shouldUseOpenapiValidation(expectedType)) {
302
- const res = (0, validateOpenApiSchema_js_1.validateObject)(paramValue, expectedType);
302
+ const res = (0, validateOpenApiSchema_js_1.validateObject)(paramValue, expectedType, { coerceTypes: true });
303
303
  if (res.isValid) {
304
304
  return res.data;
305
305
  }
@@ -38,8 +38,6 @@ import addFormats from 'ajv-formats';
38
38
  export default function validateOpenApiSchema(data, openapiSchema, options = {}) {
39
39
  return validateObject(data, openapiSchema, {
40
40
  removeAdditional: 'failing', // Remove properties that fail validation
41
- useDefaults: true,
42
- coerceTypes: true,
43
41
  ...options,
44
42
  });
45
43
  }
@@ -104,9 +102,10 @@ export function createValidator(schema, options = {}) {
104
102
  const ajv = new Ajv({
105
103
  removeAdditional: false,
106
104
  useDefaults: true,
107
- coerceTypes: true,
108
- strict: false, // Allow unknown keywords for OpenAPI compatibility
109
- allErrors: options.allErrors || false, // Collect all errors, not just the first one
105
+ strict: false,
106
+ coerceTypes: false,
107
+ strictTypes: true,
108
+ allErrors: false,
110
109
  validateFormats: true, // Enable format validation for date-time, email, etc.
111
110
  ...ajvOptions,
112
111
  });
@@ -204,7 +204,7 @@ export default class OpenapiPayloadValidator {
204
204
  validateOrFail(
205
205
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
206
206
  data, openapiSchema, target) {
207
- const validationResults = validateOpenApiSchema(data || {}, this.addComponentsToSchema(openapiSchema), this.getAjvOptions());
207
+ const validationResults = validateOpenApiSchema(data || {}, this.addComponentsToSchema(openapiSchema), this.getAjvOptions(target));
208
208
  if (!validationResults.isValid) {
209
209
  const errorClass = target === 'responseBody' ? OpenapiResponseValidationFailure : OpenapiRequestValidationFailure;
210
210
  throw new errorClass(validationResults.errors || [], target);
@@ -237,12 +237,17 @@ export default class OpenapiPayloadValidator {
237
237
  * which is automatically handled by psychic and turned into a 400
238
238
  * with the respective errors attached.
239
239
  */
240
- getAjvOptions() {
240
+ getAjvOptions(target) {
241
241
  const rendererOpts = this.openapiEndpointRenderer['validate']?.['ajvOptions'];
242
242
  const openapiConf = PsychicApp.getOrFail().openapi?.[this.openapiName];
243
243
  return {
244
244
  ...openapiConf?.validate?.ajvOptions,
245
245
  ...rendererOpts,
246
+ // if headers or query, we want to levarage ajv type coercion,
247
+ // so that a query or header parameter can have a type of i.e. number
248
+ // or boolean without failing to coerce, since by default our
249
+ // ajv instance will be instantiated with coerceTypes=false
250
+ coerceTypes: target === 'headers' || target === 'query',
246
251
  };
247
252
  }
248
253
  }
@@ -294,7 +294,7 @@ export default class Params {
294
294
  return paramValue.map(param => this.cast(paramName, param, arrayTypeToNonArrayType(expectedType), { ...opts, allowNull: true }));
295
295
  default:
296
296
  if (this.shouldUseOpenapiValidation(expectedType)) {
297
- const res = validateObject(paramValue, expectedType);
297
+ const res = validateObject(paramValue, expectedType, { coerceTypes: true });
298
298
  if (res.isValid) {
299
299
  return res.data;
300
300
  }
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.8.0",
5
+ "version": "1.8.1",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -96,4 +96,4 @@
96
96
  "winston": "^3.14.2"
97
97
  },
98
98
  "packageManager": "yarn@4.7.0"
99
- }
99
+ }