@rvoh/psychic 3.8.6 → 3.10.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/dist/cjs/src/bin/helpers/OpenApiSpecDiff.js +4 -0
- package/dist/cjs/src/openapi-renderer/app.js +2 -0
- package/dist/cjs/src/openapi-renderer/body-segment.js +3 -0
- package/dist/cjs/src/openapi-renderer/endpoint.js +10 -0
- package/dist/cjs/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +2 -0
- package/dist/cjs/src/openapi-renderer/helpers/buildDreamRequestBodyShape.js +8 -2
- package/dist/cjs/src/openapi-renderer/helpers/dreamColumnOpenapiShape.js +1 -0
- package/dist/cjs/src/openapi-renderer/helpers/legacyImplicitRequestBodyParamsConfig.js +9 -0
- package/dist/cjs/src/psychic-app/index.js +6 -3
- package/dist/cjs/src/server/params.js +78 -7
- package/dist/esm/src/bin/helpers/OpenApiSpecDiff.js +4 -0
- package/dist/esm/src/openapi-renderer/app.js +2 -0
- package/dist/esm/src/openapi-renderer/body-segment.js +3 -0
- package/dist/esm/src/openapi-renderer/endpoint.js +10 -0
- package/dist/esm/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +2 -0
- package/dist/esm/src/openapi-renderer/helpers/buildDreamRequestBodyShape.js +8 -2
- package/dist/esm/src/openapi-renderer/helpers/dreamColumnOpenapiShape.js +1 -0
- package/dist/esm/src/openapi-renderer/helpers/legacyImplicitRequestBodyParamsConfig.js +9 -0
- package/dist/esm/src/psychic-app/index.js +6 -3
- package/dist/esm/src/server/params.js +78 -7
- package/dist/types/src/bin/helpers/OpenApiSpecDiff.d.ts +5 -1
- package/dist/types/src/openapi-renderer/body-segment.d.ts +1 -0
- package/dist/types/src/openapi-renderer/endpoint.d.ts +1 -0
- package/dist/types/src/openapi-renderer/helpers/buildDreamRequestBodyShape.d.ts +7 -0
- package/dist/types/src/openapi-renderer/helpers/legacyImplicitRequestBodyParamsConfig.d.ts +6 -0
- package/dist/types/src/psychic-app/index.d.ts +24 -0
- package/dist/types/src/server/params.d.ts +41 -0
- package/package.json +1 -1
|
@@ -189,6 +189,10 @@ export class OpenApiSpecDiff {
|
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
191
|
* Retrieves head branch content for a file
|
|
192
|
+
*
|
|
193
|
+
* Marked `protected` so tests can supply a deterministic baseline instead of
|
|
194
|
+
* the live head branch. Production code never overrides this.
|
|
195
|
+
*
|
|
192
196
|
* @param absoluteFilePath - Absolute path to the file
|
|
193
197
|
*/
|
|
194
198
|
getHeadBranchContent(absoluteFilePath) {
|
|
@@ -7,6 +7,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
7
7
|
import { HttpMethods } from '../router/types.js';
|
|
8
8
|
import { DEFAULT_OPENAPI_COMPONENT_RESPONSES, DEFAULT_OPENAPI_COMPONENT_SCHEMAS } from './defaults.js';
|
|
9
9
|
import { MissingControllerActionPairingInRoutes, } from './endpoint.js';
|
|
10
|
+
import legacyImplicitRequestBodyParamsConfig from './helpers/legacyImplicitRequestBodyParamsConfig.js';
|
|
10
11
|
import suppressResponseEnumsConfig from './helpers/suppressResponseEnumsConfig.js';
|
|
11
12
|
const debugEnabled = debuglog('psychic').enabled;
|
|
12
13
|
export default class OpenapiAppRenderer {
|
|
@@ -47,6 +48,7 @@ export default class OpenapiAppRenderer {
|
|
|
47
48
|
const renderOpts = {
|
|
48
49
|
casing: 'camel',
|
|
49
50
|
suppressResponseEnums: suppressResponseEnumsConfig(openapiName),
|
|
51
|
+
legacyImplicitRequestBodyParams: legacyImplicitRequestBodyParamsConfig(openapiName),
|
|
50
52
|
};
|
|
51
53
|
const alreadyExtractedDescendantSerializers = {};
|
|
52
54
|
const renderedSchemasOpenapi = {};
|
|
@@ -54,6 +54,7 @@ export default class OpenapiSegmentExpander {
|
|
|
54
54
|
bodySegment;
|
|
55
55
|
casing;
|
|
56
56
|
suppressResponseEnums;
|
|
57
|
+
legacyImplicitRequestBodyParams;
|
|
57
58
|
target;
|
|
58
59
|
source;
|
|
59
60
|
/**
|
|
@@ -66,6 +67,7 @@ export default class OpenapiSegmentExpander {
|
|
|
66
67
|
this.bodySegment = bodySegment;
|
|
67
68
|
this.casing = renderOpts.casing;
|
|
68
69
|
this.suppressResponseEnums = renderOpts.suppressResponseEnums;
|
|
70
|
+
this.legacyImplicitRequestBodyParams = renderOpts.legacyImplicitRequestBodyParams;
|
|
69
71
|
this.target = target;
|
|
70
72
|
this.source = source ?? 'unknown';
|
|
71
73
|
}
|
|
@@ -489,6 +491,7 @@ The following values will be allowed:
|
|
|
489
491
|
including: ref.including,
|
|
490
492
|
required: ref.required,
|
|
491
493
|
combining: ref.combining,
|
|
494
|
+
legacyImplicitRequestBodyParams: this.legacyImplicitRequestBodyParams,
|
|
492
495
|
}, this.source);
|
|
493
496
|
return this.recursivelyParseBody(paramsShape);
|
|
494
497
|
}
|
|
@@ -529,7 +529,17 @@ export default class OpenapiEndpointRenderer {
|
|
|
529
529
|
including: including,
|
|
530
530
|
required: required,
|
|
531
531
|
combining: combining,
|
|
532
|
+
legacyImplicitRequestBodyParams: renderOpts.legacyImplicitRequestBodyParams,
|
|
532
533
|
}, source);
|
|
534
|
+
// When the model-derived shape contributes no properties and no `required`
|
|
535
|
+
// (e.g. the new none-default with no `params`/`only`/`combining`), there is
|
|
536
|
+
// nothing meaningful to advertise. Fall back to `defaultRequestBody()`, which
|
|
537
|
+
// omits the body entirely unless body-level pagination params are present.
|
|
538
|
+
const shapeProperties = paramsShape.properties;
|
|
539
|
+
const shapeRequired = paramsShape.required;
|
|
540
|
+
if (Object.keys(shapeProperties ?? {}).length === 0 && !shapeRequired) {
|
|
541
|
+
return this.defaultRequestBody();
|
|
542
|
+
}
|
|
533
543
|
let processedSchema = new OpenapiSegmentExpander(paramsShape, {
|
|
534
544
|
renderOpts,
|
|
535
545
|
target: 'request',
|
|
@@ -2,6 +2,7 @@ import OpenapiRequestValidationFailure from '../../error/openapi/OpenapiRequestV
|
|
|
2
2
|
import OpenapiResponseValidationFailure from '../../error/openapi/OpenapResponseValidationFailure.js';
|
|
3
3
|
import { createValidator, formatAjvErrors, } from '../../helpers/validateOpenApiSchema.js';
|
|
4
4
|
import PsychicApp from '../../psychic-app/index.js';
|
|
5
|
+
import legacyImplicitRequestBodyParamsConfig from './legacyImplicitRequestBodyParamsConfig.js';
|
|
5
6
|
import suppressResponseEnumsConfig from './suppressResponseEnumsConfig.js';
|
|
6
7
|
import { cacheValidator, getCachedValidator } from './validator-cache.js';
|
|
7
8
|
/**
|
|
@@ -207,6 +208,7 @@ export default class OpenapiPayloadValidator {
|
|
|
207
208
|
return {
|
|
208
209
|
casing: 'camel',
|
|
209
210
|
suppressResponseEnums: suppressResponseEnumsConfig(this.openapiName),
|
|
211
|
+
legacyImplicitRequestBodyParams: legacyImplicitRequestBodyParamsConfig(this.openapiName),
|
|
210
212
|
};
|
|
211
213
|
}
|
|
212
214
|
/**
|
|
@@ -11,9 +11,15 @@ import { dreamColumnOpenapiShape } from './dreamColumnOpenapiShape.js';
|
|
|
11
11
|
* truth keeps top-level and nested semantics identical.
|
|
12
12
|
*/
|
|
13
13
|
export default function buildDreamRequestBodyShape(dreamClass, opts, source) {
|
|
14
|
-
const { params, only, including, required, combining } = opts;
|
|
14
|
+
const { params, only, including, required, combining, legacyImplicitRequestBodyParams } = opts;
|
|
15
|
+
// When neither `params` nor `only` is provided, the default is to resolve to
|
|
16
|
+
// NO model columns (an empty allowlist), rather than implicitly exposing every
|
|
17
|
+
// param-safe column. The legacy implicit-all behavior can be re-activated via
|
|
18
|
+
// the `legacyImplicitRequestBodyParams` opt-in.
|
|
19
|
+
const resolvedOnly = params ?? only;
|
|
20
|
+
const onlyForResolution = resolvedOnly === undefined && !legacyImplicitRequestBodyParams ? [] : resolvedOnly;
|
|
15
21
|
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, {
|
|
16
|
-
only:
|
|
22
|
+
only: onlyForResolution,
|
|
17
23
|
including,
|
|
18
24
|
});
|
|
19
25
|
const paramsShape = {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import openapiOpts from './openapiOpts.js';
|
|
2
|
+
/**
|
|
3
|
+
* returns whether the named openapi config opts in to the legacy behavior of
|
|
4
|
+
* implicitly exposing all param-safe columns when a model-derived request body
|
|
5
|
+
* is given no `params`/`only`.
|
|
6
|
+
*/
|
|
7
|
+
export default function legacyImplicitRequestBodyParamsConfig(openapiName) {
|
|
8
|
+
return !!openapiOpts(openapiName)?.legacyImplicitRequestBodyParams;
|
|
9
|
+
}
|
|
@@ -44,7 +44,9 @@ export default class PsychicApp {
|
|
|
44
44
|
if (!psychicApp.routesCb)
|
|
45
45
|
throw new PsychicAppInitMissingRoutesCallback();
|
|
46
46
|
if (psychicApp.encryption?.cookies?.current)
|
|
47
|
-
this.checkEncryptionKey('cookies', psychicApp.encryption.cookies.current.key, psychicApp.encryption.cookies.current.algorithm);
|
|
47
|
+
this.checkEncryptionKey('cookies', 'current', psychicApp.encryption.cookies.current.key, psychicApp.encryption.cookies.current.algorithm);
|
|
48
|
+
if (psychicApp.encryption?.cookies?.legacy)
|
|
49
|
+
this.checkEncryptionKey('cookies', 'legacy', psychicApp.encryption.cookies.legacy.key, psychicApp.encryption.cookies.legacy.algorithm);
|
|
48
50
|
await psychicApp.inflections?.();
|
|
49
51
|
dreamApp.on('repl:start', context => {
|
|
50
52
|
const psychicApp = PsychicApp.getOrFail();
|
|
@@ -187,14 +189,15 @@ export default class PsychicApp {
|
|
|
187
189
|
* are not broken by a hard failure.
|
|
188
190
|
*
|
|
189
191
|
* @param encryptionIdentifier - currently must be 'cookies', though this may change in the future
|
|
192
|
+
* @param keyType - whether this is the 'current' or 'legacy' key (named in the error so a misconfigured legacy key is not mistaken for the current one)
|
|
190
193
|
* @param key - the encryption key you want to check
|
|
191
194
|
* @param algorithm - the encryption algorithm you want to check
|
|
192
195
|
*/
|
|
193
|
-
static checkEncryptionKey(encryptionIdentifier, key, algorithm) {
|
|
196
|
+
static checkEncryptionKey(encryptionIdentifier, keyType, key, algorithm) {
|
|
194
197
|
if (Encrypt.validateKey(key, algorithm))
|
|
195
198
|
return;
|
|
196
199
|
const message = `
|
|
197
|
-
Your
|
|
200
|
+
Your ${keyType} key value for ${encryptionIdentifier} encryption is invalid.
|
|
198
201
|
Try setting it to something valid, like:
|
|
199
202
|
${Encrypt.generateKey(algorithm)}
|
|
200
203
|
|
|
@@ -59,8 +59,11 @@ export default class Params {
|
|
|
59
59
|
else if (columnMetadata?.enumValues) {
|
|
60
60
|
const paramValue = params[columnName];
|
|
61
61
|
if (columnMetadata.isArray) {
|
|
62
|
+
// Single-value query arrays are conformed to arrays upstream in
|
|
63
|
+
// conformQueryArrayParamsToOpenapiShape, so a non-array reaching
|
|
64
|
+
// here is a real error rather than something to coerce.
|
|
62
65
|
if (!Array.isArray(paramValue))
|
|
63
|
-
|
|
66
|
+
throw new ParamValidationError(columnName.toString(), ['expected an array of enum values']);
|
|
64
67
|
returnObj[columnName] = paramValue.map(p => {
|
|
65
68
|
return new this(params).cast(columnName.toString(), p, 'string', {
|
|
66
69
|
allowNull: columnMetadata.allowNull,
|
|
@@ -177,10 +180,16 @@ export default class Params {
|
|
|
177
180
|
return paramValue;
|
|
178
181
|
}
|
|
179
182
|
const integerRegexp = /^-?\d+$/;
|
|
183
|
+
// Plain decimal numeric string: optional sign, digits with an optional
|
|
184
|
+
// fractional part (or a bare fractional part), and an optional decimal
|
|
185
|
+
// exponent. Deliberately excludes hex/octal/binary literals and the
|
|
186
|
+
// "Infinity"/"NaN" keywords that `Number()` would otherwise accept.
|
|
187
|
+
const decimalNumberRegexp = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
180
188
|
switch (expectedType) {
|
|
181
189
|
case 'string':
|
|
182
190
|
if (typeof paramValue !== 'string')
|
|
183
191
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
192
|
+
this.throwUnlessWithinLength(paramName, paramValue, opts);
|
|
184
193
|
if (opts?.enum && !opts.enum.includes(paramValue))
|
|
185
194
|
throw new ParamValidationError(paramName, ['did not match expected enum values']);
|
|
186
195
|
if (opts?.match)
|
|
@@ -191,6 +200,7 @@ export default class Params {
|
|
|
191
200
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
192
201
|
if (!integerRegexp.test(paramValue?.toString()))
|
|
193
202
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
203
|
+
this.throwUnlessWithinRange(paramName, BigInt(paramValue.toString()), opts);
|
|
194
204
|
return paramValue.toString();
|
|
195
205
|
case 'boolean':
|
|
196
206
|
if ([true, 'true', 1, '1'].includes(paramValue))
|
|
@@ -254,25 +264,47 @@ export default class Params {
|
|
|
254
264
|
}
|
|
255
265
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
256
266
|
}
|
|
257
|
-
case 'integer':
|
|
267
|
+
case 'integer': {
|
|
258
268
|
if (typeof paramValue !== 'string' && typeof paramValue !== 'number')
|
|
259
269
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
260
270
|
if (!integerRegexp.test(paramValue?.toString()))
|
|
261
271
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
262
|
-
|
|
272
|
+
const integerValue = parseInt(paramValue, 10);
|
|
273
|
+
// Reject values outside the safe-integer range: a long digit string
|
|
274
|
+
// (e.g. a 40-digit number) would otherwise be silently rounded to a
|
|
275
|
+
// nearby float by parseInt. Genuinely large integers should be cast as
|
|
276
|
+
// `bigint`, which preserves the exact value as a string.
|
|
277
|
+
if (!Number.isSafeInteger(integerValue))
|
|
278
|
+
throw new ParamValidationError(paramName, [
|
|
279
|
+
'expected an integer within the safe integer range (use bigint for larger values)',
|
|
280
|
+
]);
|
|
281
|
+
this.throwUnlessWithinRange(paramName, integerValue, opts);
|
|
282
|
+
return integerValue;
|
|
283
|
+
}
|
|
263
284
|
case 'json':
|
|
264
285
|
if (typeof paramValue !== 'object')
|
|
265
286
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
266
287
|
return paramValue;
|
|
267
|
-
case 'number':
|
|
268
|
-
if (typeof paramValue === 'number')
|
|
288
|
+
case 'number': {
|
|
289
|
+
if (typeof paramValue === 'number') {
|
|
290
|
+
if (!Number.isFinite(paramValue))
|
|
291
|
+
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
292
|
+
this.throwUnlessWithinRange(paramName, paramValue, opts);
|
|
269
293
|
return paramValue;
|
|
294
|
+
}
|
|
270
295
|
if (typeof paramValue === 'string') {
|
|
271
|
-
|
|
296
|
+
// Require a plain decimal numeric string and a finite result. This
|
|
297
|
+
// rejects hex/octal/binary forms ("0x10"), the literals "Infinity"/
|
|
298
|
+
// "NaN", and magnitudes that overflow to Infinity ("1e999"), all of
|
|
299
|
+
// which `Number()` would otherwise silently coerce.
|
|
300
|
+
const numberValue = Number(paramValue);
|
|
301
|
+
if (!decimalNumberRegexp.test(paramValue) || !Number.isFinite(numberValue))
|
|
272
302
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
273
|
-
|
|
303
|
+
this.throwUnlessWithinRange(paramName, numberValue, opts);
|
|
304
|
+
return numberValue;
|
|
274
305
|
}
|
|
275
306
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
307
|
+
}
|
|
276
308
|
case 'null':
|
|
277
309
|
if (paramValue !== null)
|
|
278
310
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
@@ -352,6 +384,45 @@ export default class Params {
|
|
|
352
384
|
return paramValue;
|
|
353
385
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
354
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Enforces `minLength`/`maxLength` (inclusive) against an already-coerced
|
|
389
|
+
* string value, throwing {@link ParamValidationError} on violation. Mirrors
|
|
390
|
+
* JSON-schema / OpenAPI string-length constraints.
|
|
391
|
+
*/
|
|
392
|
+
throwUnlessWithinLength(paramName, paramValue, opts) {
|
|
393
|
+
if (opts?.minLength !== undefined && paramValue.length < opts.minLength)
|
|
394
|
+
throw new ParamValidationError(paramName, [
|
|
395
|
+
`expected string with length no less than ${opts.minLength}`,
|
|
396
|
+
]);
|
|
397
|
+
if (opts?.maxLength !== undefined && paramValue.length > opts.maxLength)
|
|
398
|
+
throw new ParamValidationError(paramName, [
|
|
399
|
+
`expected string with length no greater than ${opts.maxLength}`,
|
|
400
|
+
]);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Enforces `minimum`/`maximum` (inclusive) against an already-coerced numeric
|
|
404
|
+
* value, throwing {@link ParamValidationError} on violation. Accepts a
|
|
405
|
+
* `bigint` so `bigint` casts are checked without precision loss; the bounds
|
|
406
|
+
* themselves are compared as `bigint` when they are integers and otherwise
|
|
407
|
+
* fall back to floating-point comparison. Mirrors JSON-schema / OpenAPI
|
|
408
|
+
* numeric-range constraints.
|
|
409
|
+
*/
|
|
410
|
+
throwUnlessWithinRange(paramName, paramValue, opts) {
|
|
411
|
+
if (opts?.minimum !== undefined && this.isBelowNumericBound(paramValue, opts.minimum))
|
|
412
|
+
throw new ParamValidationError(paramName, [`expected a value no less than ${opts.minimum}`]);
|
|
413
|
+
if (opts?.maximum !== undefined && this.isAboveNumericBound(paramValue, opts.maximum))
|
|
414
|
+
throw new ParamValidationError(paramName, [`expected a value no greater than ${opts.maximum}`]);
|
|
415
|
+
}
|
|
416
|
+
isBelowNumericBound(value, bound) {
|
|
417
|
+
if (typeof value === 'bigint')
|
|
418
|
+
return Number.isInteger(bound) ? value < BigInt(bound) : Number(value) < bound;
|
|
419
|
+
return value < bound;
|
|
420
|
+
}
|
|
421
|
+
isAboveNumericBound(value, bound) {
|
|
422
|
+
if (typeof value === 'bigint')
|
|
423
|
+
return Number.isInteger(bound) ? value > BigInt(bound) : Number(value) > bound;
|
|
424
|
+
return value > bound;
|
|
425
|
+
}
|
|
355
426
|
throwUnlessAllowNull(paramName, paramValue, message, { allowNull = false } = {}) {
|
|
356
427
|
const isNullOrUndefined = [null, undefined].includes(paramValue);
|
|
357
428
|
if (allowNull && isNullOrUndefined)
|
|
@@ -189,6 +189,10 @@ export class OpenApiSpecDiff {
|
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
191
|
* Retrieves head branch content for a file
|
|
192
|
+
*
|
|
193
|
+
* Marked `protected` so tests can supply a deterministic baseline instead of
|
|
194
|
+
* the live head branch. Production code never overrides this.
|
|
195
|
+
*
|
|
192
196
|
* @param absoluteFilePath - Absolute path to the file
|
|
193
197
|
*/
|
|
194
198
|
getHeadBranchContent(absoluteFilePath) {
|
|
@@ -7,6 +7,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
7
7
|
import { HttpMethods } from '../router/types.js';
|
|
8
8
|
import { DEFAULT_OPENAPI_COMPONENT_RESPONSES, DEFAULT_OPENAPI_COMPONENT_SCHEMAS } from './defaults.js';
|
|
9
9
|
import { MissingControllerActionPairingInRoutes, } from './endpoint.js';
|
|
10
|
+
import legacyImplicitRequestBodyParamsConfig from './helpers/legacyImplicitRequestBodyParamsConfig.js';
|
|
10
11
|
import suppressResponseEnumsConfig from './helpers/suppressResponseEnumsConfig.js';
|
|
11
12
|
const debugEnabled = debuglog('psychic').enabled;
|
|
12
13
|
export default class OpenapiAppRenderer {
|
|
@@ -47,6 +48,7 @@ export default class OpenapiAppRenderer {
|
|
|
47
48
|
const renderOpts = {
|
|
48
49
|
casing: 'camel',
|
|
49
50
|
suppressResponseEnums: suppressResponseEnumsConfig(openapiName),
|
|
51
|
+
legacyImplicitRequestBodyParams: legacyImplicitRequestBodyParamsConfig(openapiName),
|
|
50
52
|
};
|
|
51
53
|
const alreadyExtractedDescendantSerializers = {};
|
|
52
54
|
const renderedSchemasOpenapi = {};
|
|
@@ -54,6 +54,7 @@ export default class OpenapiSegmentExpander {
|
|
|
54
54
|
bodySegment;
|
|
55
55
|
casing;
|
|
56
56
|
suppressResponseEnums;
|
|
57
|
+
legacyImplicitRequestBodyParams;
|
|
57
58
|
target;
|
|
58
59
|
source;
|
|
59
60
|
/**
|
|
@@ -66,6 +67,7 @@ export default class OpenapiSegmentExpander {
|
|
|
66
67
|
this.bodySegment = bodySegment;
|
|
67
68
|
this.casing = renderOpts.casing;
|
|
68
69
|
this.suppressResponseEnums = renderOpts.suppressResponseEnums;
|
|
70
|
+
this.legacyImplicitRequestBodyParams = renderOpts.legacyImplicitRequestBodyParams;
|
|
69
71
|
this.target = target;
|
|
70
72
|
this.source = source ?? 'unknown';
|
|
71
73
|
}
|
|
@@ -489,6 +491,7 @@ The following values will be allowed:
|
|
|
489
491
|
including: ref.including,
|
|
490
492
|
required: ref.required,
|
|
491
493
|
combining: ref.combining,
|
|
494
|
+
legacyImplicitRequestBodyParams: this.legacyImplicitRequestBodyParams,
|
|
492
495
|
}, this.source);
|
|
493
496
|
return this.recursivelyParseBody(paramsShape);
|
|
494
497
|
}
|
|
@@ -529,7 +529,17 @@ export default class OpenapiEndpointRenderer {
|
|
|
529
529
|
including: including,
|
|
530
530
|
required: required,
|
|
531
531
|
combining: combining,
|
|
532
|
+
legacyImplicitRequestBodyParams: renderOpts.legacyImplicitRequestBodyParams,
|
|
532
533
|
}, source);
|
|
534
|
+
// When the model-derived shape contributes no properties and no `required`
|
|
535
|
+
// (e.g. the new none-default with no `params`/`only`/`combining`), there is
|
|
536
|
+
// nothing meaningful to advertise. Fall back to `defaultRequestBody()`, which
|
|
537
|
+
// omits the body entirely unless body-level pagination params are present.
|
|
538
|
+
const shapeProperties = paramsShape.properties;
|
|
539
|
+
const shapeRequired = paramsShape.required;
|
|
540
|
+
if (Object.keys(shapeProperties ?? {}).length === 0 && !shapeRequired) {
|
|
541
|
+
return this.defaultRequestBody();
|
|
542
|
+
}
|
|
533
543
|
let processedSchema = new OpenapiSegmentExpander(paramsShape, {
|
|
534
544
|
renderOpts,
|
|
535
545
|
target: 'request',
|
|
@@ -2,6 +2,7 @@ import OpenapiRequestValidationFailure from '../../error/openapi/OpenapiRequestV
|
|
|
2
2
|
import OpenapiResponseValidationFailure from '../../error/openapi/OpenapResponseValidationFailure.js';
|
|
3
3
|
import { createValidator, formatAjvErrors, } from '../../helpers/validateOpenApiSchema.js';
|
|
4
4
|
import PsychicApp from '../../psychic-app/index.js';
|
|
5
|
+
import legacyImplicitRequestBodyParamsConfig from './legacyImplicitRequestBodyParamsConfig.js';
|
|
5
6
|
import suppressResponseEnumsConfig from './suppressResponseEnumsConfig.js';
|
|
6
7
|
import { cacheValidator, getCachedValidator } from './validator-cache.js';
|
|
7
8
|
/**
|
|
@@ -207,6 +208,7 @@ export default class OpenapiPayloadValidator {
|
|
|
207
208
|
return {
|
|
208
209
|
casing: 'camel',
|
|
209
210
|
suppressResponseEnums: suppressResponseEnumsConfig(this.openapiName),
|
|
211
|
+
legacyImplicitRequestBodyParams: legacyImplicitRequestBodyParamsConfig(this.openapiName),
|
|
210
212
|
};
|
|
211
213
|
}
|
|
212
214
|
/**
|
|
@@ -11,9 +11,15 @@ import { dreamColumnOpenapiShape } from './dreamColumnOpenapiShape.js';
|
|
|
11
11
|
* truth keeps top-level and nested semantics identical.
|
|
12
12
|
*/
|
|
13
13
|
export default function buildDreamRequestBodyShape(dreamClass, opts, source) {
|
|
14
|
-
const { params, only, including, required, combining } = opts;
|
|
14
|
+
const { params, only, including, required, combining, legacyImplicitRequestBodyParams } = opts;
|
|
15
|
+
// When neither `params` nor `only` is provided, the default is to resolve to
|
|
16
|
+
// NO model columns (an empty allowlist), rather than implicitly exposing every
|
|
17
|
+
// param-safe column. The legacy implicit-all behavior can be re-activated via
|
|
18
|
+
// the `legacyImplicitRequestBodyParams` opt-in.
|
|
19
|
+
const resolvedOnly = params ?? only;
|
|
20
|
+
const onlyForResolution = resolvedOnly === undefined && !legacyImplicitRequestBodyParams ? [] : resolvedOnly;
|
|
15
21
|
const paramSafeColumns = openapiParamNamesForDreamClass(dreamClass, {
|
|
16
|
-
only:
|
|
22
|
+
only: onlyForResolution,
|
|
17
23
|
including,
|
|
18
24
|
});
|
|
19
25
|
const paramsShape = {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import openapiOpts from './openapiOpts.js';
|
|
2
|
+
/**
|
|
3
|
+
* returns whether the named openapi config opts in to the legacy behavior of
|
|
4
|
+
* implicitly exposing all param-safe columns when a model-derived request body
|
|
5
|
+
* is given no `params`/`only`.
|
|
6
|
+
*/
|
|
7
|
+
export default function legacyImplicitRequestBodyParamsConfig(openapiName) {
|
|
8
|
+
return !!openapiOpts(openapiName)?.legacyImplicitRequestBodyParams;
|
|
9
|
+
}
|
|
@@ -44,7 +44,9 @@ export default class PsychicApp {
|
|
|
44
44
|
if (!psychicApp.routesCb)
|
|
45
45
|
throw new PsychicAppInitMissingRoutesCallback();
|
|
46
46
|
if (psychicApp.encryption?.cookies?.current)
|
|
47
|
-
this.checkEncryptionKey('cookies', psychicApp.encryption.cookies.current.key, psychicApp.encryption.cookies.current.algorithm);
|
|
47
|
+
this.checkEncryptionKey('cookies', 'current', psychicApp.encryption.cookies.current.key, psychicApp.encryption.cookies.current.algorithm);
|
|
48
|
+
if (psychicApp.encryption?.cookies?.legacy)
|
|
49
|
+
this.checkEncryptionKey('cookies', 'legacy', psychicApp.encryption.cookies.legacy.key, psychicApp.encryption.cookies.legacy.algorithm);
|
|
48
50
|
await psychicApp.inflections?.();
|
|
49
51
|
dreamApp.on('repl:start', context => {
|
|
50
52
|
const psychicApp = PsychicApp.getOrFail();
|
|
@@ -187,14 +189,15 @@ export default class PsychicApp {
|
|
|
187
189
|
* are not broken by a hard failure.
|
|
188
190
|
*
|
|
189
191
|
* @param encryptionIdentifier - currently must be 'cookies', though this may change in the future
|
|
192
|
+
* @param keyType - whether this is the 'current' or 'legacy' key (named in the error so a misconfigured legacy key is not mistaken for the current one)
|
|
190
193
|
* @param key - the encryption key you want to check
|
|
191
194
|
* @param algorithm - the encryption algorithm you want to check
|
|
192
195
|
*/
|
|
193
|
-
static checkEncryptionKey(encryptionIdentifier, key, algorithm) {
|
|
196
|
+
static checkEncryptionKey(encryptionIdentifier, keyType, key, algorithm) {
|
|
194
197
|
if (Encrypt.validateKey(key, algorithm))
|
|
195
198
|
return;
|
|
196
199
|
const message = `
|
|
197
|
-
Your
|
|
200
|
+
Your ${keyType} key value for ${encryptionIdentifier} encryption is invalid.
|
|
198
201
|
Try setting it to something valid, like:
|
|
199
202
|
${Encrypt.generateKey(algorithm)}
|
|
200
203
|
|
|
@@ -59,8 +59,11 @@ export default class Params {
|
|
|
59
59
|
else if (columnMetadata?.enumValues) {
|
|
60
60
|
const paramValue = params[columnName];
|
|
61
61
|
if (columnMetadata.isArray) {
|
|
62
|
+
// Single-value query arrays are conformed to arrays upstream in
|
|
63
|
+
// conformQueryArrayParamsToOpenapiShape, so a non-array reaching
|
|
64
|
+
// here is a real error rather than something to coerce.
|
|
62
65
|
if (!Array.isArray(paramValue))
|
|
63
|
-
|
|
66
|
+
throw new ParamValidationError(columnName.toString(), ['expected an array of enum values']);
|
|
64
67
|
returnObj[columnName] = paramValue.map(p => {
|
|
65
68
|
return new this(params).cast(columnName.toString(), p, 'string', {
|
|
66
69
|
allowNull: columnMetadata.allowNull,
|
|
@@ -177,10 +180,16 @@ export default class Params {
|
|
|
177
180
|
return paramValue;
|
|
178
181
|
}
|
|
179
182
|
const integerRegexp = /^-?\d+$/;
|
|
183
|
+
// Plain decimal numeric string: optional sign, digits with an optional
|
|
184
|
+
// fractional part (or a bare fractional part), and an optional decimal
|
|
185
|
+
// exponent. Deliberately excludes hex/octal/binary literals and the
|
|
186
|
+
// "Infinity"/"NaN" keywords that `Number()` would otherwise accept.
|
|
187
|
+
const decimalNumberRegexp = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
180
188
|
switch (expectedType) {
|
|
181
189
|
case 'string':
|
|
182
190
|
if (typeof paramValue !== 'string')
|
|
183
191
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
192
|
+
this.throwUnlessWithinLength(paramName, paramValue, opts);
|
|
184
193
|
if (opts?.enum && !opts.enum.includes(paramValue))
|
|
185
194
|
throw new ParamValidationError(paramName, ['did not match expected enum values']);
|
|
186
195
|
if (opts?.match)
|
|
@@ -191,6 +200,7 @@ export default class Params {
|
|
|
191
200
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
192
201
|
if (!integerRegexp.test(paramValue?.toString()))
|
|
193
202
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
203
|
+
this.throwUnlessWithinRange(paramName, BigInt(paramValue.toString()), opts);
|
|
194
204
|
return paramValue.toString();
|
|
195
205
|
case 'boolean':
|
|
196
206
|
if ([true, 'true', 1, '1'].includes(paramValue))
|
|
@@ -254,25 +264,47 @@ export default class Params {
|
|
|
254
264
|
}
|
|
255
265
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
256
266
|
}
|
|
257
|
-
case 'integer':
|
|
267
|
+
case 'integer': {
|
|
258
268
|
if (typeof paramValue !== 'string' && typeof paramValue !== 'number')
|
|
259
269
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
260
270
|
if (!integerRegexp.test(paramValue?.toString()))
|
|
261
271
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
262
|
-
|
|
272
|
+
const integerValue = parseInt(paramValue, 10);
|
|
273
|
+
// Reject values outside the safe-integer range: a long digit string
|
|
274
|
+
// (e.g. a 40-digit number) would otherwise be silently rounded to a
|
|
275
|
+
// nearby float by parseInt. Genuinely large integers should be cast as
|
|
276
|
+
// `bigint`, which preserves the exact value as a string.
|
|
277
|
+
if (!Number.isSafeInteger(integerValue))
|
|
278
|
+
throw new ParamValidationError(paramName, [
|
|
279
|
+
'expected an integer within the safe integer range (use bigint for larger values)',
|
|
280
|
+
]);
|
|
281
|
+
this.throwUnlessWithinRange(paramName, integerValue, opts);
|
|
282
|
+
return integerValue;
|
|
283
|
+
}
|
|
263
284
|
case 'json':
|
|
264
285
|
if (typeof paramValue !== 'object')
|
|
265
286
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
266
287
|
return paramValue;
|
|
267
|
-
case 'number':
|
|
268
|
-
if (typeof paramValue === 'number')
|
|
288
|
+
case 'number': {
|
|
289
|
+
if (typeof paramValue === 'number') {
|
|
290
|
+
if (!Number.isFinite(paramValue))
|
|
291
|
+
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
292
|
+
this.throwUnlessWithinRange(paramName, paramValue, opts);
|
|
269
293
|
return paramValue;
|
|
294
|
+
}
|
|
270
295
|
if (typeof paramValue === 'string') {
|
|
271
|
-
|
|
296
|
+
// Require a plain decimal numeric string and a finite result. This
|
|
297
|
+
// rejects hex/octal/binary forms ("0x10"), the literals "Infinity"/
|
|
298
|
+
// "NaN", and magnitudes that overflow to Infinity ("1e999"), all of
|
|
299
|
+
// which `Number()` would otherwise silently coerce.
|
|
300
|
+
const numberValue = Number(paramValue);
|
|
301
|
+
if (!decimalNumberRegexp.test(paramValue) || !Number.isFinite(numberValue))
|
|
272
302
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
273
|
-
|
|
303
|
+
this.throwUnlessWithinRange(paramName, numberValue, opts);
|
|
304
|
+
return numberValue;
|
|
274
305
|
}
|
|
275
306
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
307
|
+
}
|
|
276
308
|
case 'null':
|
|
277
309
|
if (paramValue !== null)
|
|
278
310
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
@@ -352,6 +384,45 @@ export default class Params {
|
|
|
352
384
|
return paramValue;
|
|
353
385
|
throw new ParamValidationError(paramName, [typeToError(expectedType)]);
|
|
354
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Enforces `minLength`/`maxLength` (inclusive) against an already-coerced
|
|
389
|
+
* string value, throwing {@link ParamValidationError} on violation. Mirrors
|
|
390
|
+
* JSON-schema / OpenAPI string-length constraints.
|
|
391
|
+
*/
|
|
392
|
+
throwUnlessWithinLength(paramName, paramValue, opts) {
|
|
393
|
+
if (opts?.minLength !== undefined && paramValue.length < opts.minLength)
|
|
394
|
+
throw new ParamValidationError(paramName, [
|
|
395
|
+
`expected string with length no less than ${opts.minLength}`,
|
|
396
|
+
]);
|
|
397
|
+
if (opts?.maxLength !== undefined && paramValue.length > opts.maxLength)
|
|
398
|
+
throw new ParamValidationError(paramName, [
|
|
399
|
+
`expected string with length no greater than ${opts.maxLength}`,
|
|
400
|
+
]);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Enforces `minimum`/`maximum` (inclusive) against an already-coerced numeric
|
|
404
|
+
* value, throwing {@link ParamValidationError} on violation. Accepts a
|
|
405
|
+
* `bigint` so `bigint` casts are checked without precision loss; the bounds
|
|
406
|
+
* themselves are compared as `bigint` when they are integers and otherwise
|
|
407
|
+
* fall back to floating-point comparison. Mirrors JSON-schema / OpenAPI
|
|
408
|
+
* numeric-range constraints.
|
|
409
|
+
*/
|
|
410
|
+
throwUnlessWithinRange(paramName, paramValue, opts) {
|
|
411
|
+
if (opts?.minimum !== undefined && this.isBelowNumericBound(paramValue, opts.minimum))
|
|
412
|
+
throw new ParamValidationError(paramName, [`expected a value no less than ${opts.minimum}`]);
|
|
413
|
+
if (opts?.maximum !== undefined && this.isAboveNumericBound(paramValue, opts.maximum))
|
|
414
|
+
throw new ParamValidationError(paramName, [`expected a value no greater than ${opts.maximum}`]);
|
|
415
|
+
}
|
|
416
|
+
isBelowNumericBound(value, bound) {
|
|
417
|
+
if (typeof value === 'bigint')
|
|
418
|
+
return Number.isInteger(bound) ? value < BigInt(bound) : Number(value) < bound;
|
|
419
|
+
return value < bound;
|
|
420
|
+
}
|
|
421
|
+
isAboveNumericBound(value, bound) {
|
|
422
|
+
if (typeof value === 'bigint')
|
|
423
|
+
return Number.isInteger(bound) ? value > BigInt(bound) : Number(value) > bound;
|
|
424
|
+
return value > bound;
|
|
425
|
+
}
|
|
355
426
|
throwUnlessAllowNull(paramName, paramValue, message, { allowNull = false } = {}) {
|
|
356
427
|
const isNullOrUndefined = [null, undefined].includes(paramValue);
|
|
357
428
|
if (allowNull && isNullOrUndefined)
|
|
@@ -96,9 +96,13 @@ export declare class OpenApiSpecDiff {
|
|
|
96
96
|
private createTempFilePath;
|
|
97
97
|
/**
|
|
98
98
|
* Retrieves head branch content for a file
|
|
99
|
+
*
|
|
100
|
+
* Marked `protected` so tests can supply a deterministic baseline instead of
|
|
101
|
+
* the live head branch. Production code never overrides this.
|
|
102
|
+
*
|
|
99
103
|
* @param absoluteFilePath - Absolute path to the file
|
|
100
104
|
*/
|
|
101
|
-
|
|
105
|
+
protected getHeadBranchContent(absoluteFilePath: string): string;
|
|
102
106
|
/**
|
|
103
107
|
* Compares a single OpenAPI file against head branch
|
|
104
108
|
*/
|
|
@@ -11,6 +11,7 @@ import { OpenapiBodySegment, ReferencedSerializersAndOpenapiEndpointResponse, Se
|
|
|
11
11
|
export interface OpenapiRenderOpts {
|
|
12
12
|
casing: SerializerCasing;
|
|
13
13
|
suppressResponseEnums: boolean;
|
|
14
|
+
legacyImplicitRequestBodyParams: boolean;
|
|
14
15
|
}
|
|
15
16
|
export interface ToPathObjectOpts {
|
|
16
17
|
openapiName: string;
|
|
@@ -6,6 +6,13 @@ export interface BuildDreamRequestBodyShapeOpts {
|
|
|
6
6
|
including?: readonly string[] | undefined;
|
|
7
7
|
required?: readonly string[] | undefined;
|
|
8
8
|
combining?: Record<string, unknown> | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* When true, restores the legacy behavior of resolving to ALL param-safe
|
|
11
|
+
* columns when no `params`/`only` are provided. When false/undefined (the
|
|
12
|
+
* default), an absent `params`/`only` resolves to NO model columns, so that
|
|
13
|
+
* database-level structure is not implicitly leaked into the OpenAPI shape.
|
|
14
|
+
*/
|
|
15
|
+
legacyImplicitRequestBodyParams?: boolean | undefined;
|
|
9
16
|
}
|
|
10
17
|
/**
|
|
11
18
|
* @internal
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* returns whether the named openapi config opts in to the legacy behavior of
|
|
3
|
+
* implicitly exposing all param-safe columns when a model-derived request body
|
|
4
|
+
* is given no `params`/`only`.
|
|
5
|
+
*/
|
|
6
|
+
export default function legacyImplicitRequestBodyParamsConfig(openapiName: string): boolean;
|
|
@@ -125,6 +125,7 @@ export default class PsychicApp {
|
|
|
125
125
|
* are not broken by a hard failure.
|
|
126
126
|
*
|
|
127
127
|
* @param encryptionIdentifier - currently must be 'cookies', though this may change in the future
|
|
128
|
+
* @param keyType - whether this is the 'current' or 'legacy' key (named in the error so a misconfigured legacy key is not mistaken for the current one)
|
|
128
129
|
* @param key - the encryption key you want to check
|
|
129
130
|
* @param algorithm - the encryption algorithm you want to check
|
|
130
131
|
*/
|
|
@@ -356,6 +357,29 @@ interface PsychicOpenapiBaseOptions {
|
|
|
356
357
|
* ```
|
|
357
358
|
*/
|
|
358
359
|
suppressResponseEnums?: boolean;
|
|
360
|
+
/**
|
|
361
|
+
* @deprecated escape hatch — prefer explicit `params`/`only` on each request body.
|
|
362
|
+
*
|
|
363
|
+
* When true, restores the legacy behavior where a model-derived `requestBody`
|
|
364
|
+
* (or nested `$dream`/`for:` segment) given no `params`/`only` implicitly
|
|
365
|
+
* exposes ALL param-safe columns of the model.
|
|
366
|
+
*
|
|
367
|
+
* As of 3.9.0 the default flipped: an absent `params`/`only` now resolves to
|
|
368
|
+
* NO model columns, so that database-level structure is not implicitly leaked
|
|
369
|
+
* into the OpenAPI shape (an attacker could otherwise infer the column set).
|
|
370
|
+
* A top-level model-derived `requestBody` with nothing to contribute is
|
|
371
|
+
* omitted entirely; a nested `$dream`/`for:` segment renders an empty object.
|
|
372
|
+
*
|
|
373
|
+
* Set this to true to keep the pre-3.9.0 implicit-all behavior while you
|
|
374
|
+
* migrate to explicit allowlists.
|
|
375
|
+
*
|
|
376
|
+
* ```ts
|
|
377
|
+
* psy.set('openapi', {
|
|
378
|
+
* legacyImplicitRequestBodyParams: true,
|
|
379
|
+
* })
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
legacyImplicitRequestBodyParams?: boolean;
|
|
359
383
|
/**
|
|
360
384
|
* When true, psychic will use the `openapi-typescript` package
|
|
361
385
|
* to read this openapi.json file and create typescript interfaces
|
|
@@ -77,6 +77,23 @@ export default class Params {
|
|
|
77
77
|
private validateOpenapiOrThrow;
|
|
78
78
|
restrict(allowed: string[]): PsychicParamsDictionary;
|
|
79
79
|
private matchRegexOrThrow;
|
|
80
|
+
/**
|
|
81
|
+
* Enforces `minLength`/`maxLength` (inclusive) against an already-coerced
|
|
82
|
+
* string value, throwing {@link ParamValidationError} on violation. Mirrors
|
|
83
|
+
* JSON-schema / OpenAPI string-length constraints.
|
|
84
|
+
*/
|
|
85
|
+
private throwUnlessWithinLength;
|
|
86
|
+
/**
|
|
87
|
+
* Enforces `minimum`/`maximum` (inclusive) against an already-coerced numeric
|
|
88
|
+
* value, throwing {@link ParamValidationError} on violation. Accepts a
|
|
89
|
+
* `bigint` so `bigint` casts are checked without precision loss; the bounds
|
|
90
|
+
* themselves are compared as `bigint` when they are integers and otherwise
|
|
91
|
+
* fall back to floating-point comparison. Mirrors JSON-schema / OpenAPI
|
|
92
|
+
* numeric-range constraints.
|
|
93
|
+
*/
|
|
94
|
+
private throwUnlessWithinRange;
|
|
95
|
+
private isBelowNumericBound;
|
|
96
|
+
private isAboveNumericBound;
|
|
80
97
|
private throwUnlessAllowNull;
|
|
81
98
|
}
|
|
82
99
|
export type ValidatedReturnType<ExpectedType, OptsType> = ExpectedType extends RegExp ? string : ExpectedType extends 'string' ? OptsType extends {
|
|
@@ -113,6 +130,30 @@ export type ParamsCastOptions<EnumType> = {
|
|
|
113
130
|
allowNull?: boolean;
|
|
114
131
|
match?: RegExp;
|
|
115
132
|
enum?: EnumType;
|
|
133
|
+
/**
|
|
134
|
+
* Maximum permitted string length (inclusive). Enforced after coercion for
|
|
135
|
+
* `string` casts (and element-wise for `string[]`). Mirrors JSON-schema /
|
|
136
|
+
* OpenAPI `maxLength`.
|
|
137
|
+
*/
|
|
138
|
+
maxLength?: number;
|
|
139
|
+
/**
|
|
140
|
+
* Minimum permitted string length (inclusive). Enforced after coercion for
|
|
141
|
+
* `string` casts (and element-wise for `string[]`). Mirrors JSON-schema /
|
|
142
|
+
* OpenAPI `minLength`.
|
|
143
|
+
*/
|
|
144
|
+
minLength?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Minimum permitted numeric value (inclusive). Enforced after coercion for
|
|
147
|
+
* `number`/`integer`/`bigint` casts (and element-wise for their array
|
|
148
|
+
* variants). Mirrors JSON-schema / OpenAPI `minimum`.
|
|
149
|
+
*/
|
|
150
|
+
minimum?: number;
|
|
151
|
+
/**
|
|
152
|
+
* Maximum permitted numeric value (inclusive). Enforced after coercion for
|
|
153
|
+
* `number`/`integer`/`bigint` casts (and element-wise for their array
|
|
154
|
+
* variants). Mirrors JSON-schema / OpenAPI `maximum`.
|
|
155
|
+
*/
|
|
156
|
+
maximum?: number;
|
|
116
157
|
};
|
|
117
158
|
interface ParamsForOptsBase<OnlyArray> {
|
|
118
159
|
array?: boolean;
|