From d30a243bdfd4ff1f30e75a38f9179c0b5232fc11 Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Mon, 8 Apr 2024 13:34:59 +0200 Subject: [PATCH] Don't import Joi typings --- API.md | 2 +- lib/config.js | 5 +- lib/core.js | 3 +- lib/route.js | 2 +- lib/server.js | 5 +- lib/types/route.d.ts | 73 ++++++++++++++++------------ lib/types/server/index.d.ts | 1 + lib/types/server/options.d.ts | 6 ++- lib/types/server/server.d.ts | 24 ++++----- lib/types/server/validation.d.ts | 83 ++++++++++++++++++++++++++++++++ lib/validation.js | 27 +++++------ test/validation.js | 3 +- 12 files changed, 167 insertions(+), 67 deletions(-) create mode 100644 lib/types/server/validation.d.ts diff --git a/API.md b/API.md index 40d8ffe88..cb120f67b 100755 --- a/API.md +++ b/API.md @@ -2712,7 +2712,7 @@ Registers a server validation module used to compile raw validation rules into v - `validator` - the validation module (e.g. **joi**). -Return value: none. +Return value: The `server` object. Note: the validator is only used when validation rules are not pre-compiled schemas. When a validation rules is a function or schema object, the rule is used as-is and the validator is not used. When setting a validator inside a plugin, the validator is only applied to routes set up by the plugin and plugins registered by it. diff --git a/lib/config.js b/lib/config.js index 2b668f97d..67d606a2f 100755 --- a/lib/config.js +++ b/lib/config.js @@ -229,7 +229,10 @@ internals.routeBase = Validate.object({ failAction: internals.failAction, errorFields: Validate.object(), options: Validate.object().default(), - validator: Validate.object() + validator: Validate.object({ + compile: Validate.function().required() + }) + .unknown() }) .default() }); diff --git a/lib/core.js b/lib/core.js index 43329536f..7f3dec7a2 100755 --- a/lib/core.js +++ b/lib/core.js @@ -26,7 +26,6 @@ const Request = require('./request'); const Response = require('./response'); const Route = require('./route'); const Toolkit = require('./toolkit'); -const Validation = require('./validation'); const internals = { @@ -128,7 +127,7 @@ exports = module.exports = internals.Core = class { this._initializeCache(); if (this.settings.routes.validate.validator) { - this.validator = Validation.validator(this.settings.routes.validate.validator); + this.validator = this.settings.routes.validate.validator; } this.listener = this._createListener(); diff --git a/lib/route.js b/lib/route.js index 0fb4e0d49..961a10be7 100755 --- a/lib/route.js +++ b/lib/route.js @@ -455,7 +455,7 @@ internals.config = function (chain) { let config = chain[0]; for (const item of chain) { - config = Hoek.applyToDefaults(config, item, { shallow: ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query', 'validate.state'] }); + config = Hoek.applyToDefaults(config, item, { shallow: ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query', 'validate.state', 'validate.validator'] }); } return config; diff --git a/lib/server.js b/lib/server.js index d2a5d5bf7..9dabb59ec 100755 --- a/lib/server.js +++ b/lib/server.js @@ -560,9 +560,12 @@ internals.Server = class { validator(validator) { + Hoek.assert(typeof validator?.compile === 'function', 'Validator must have a compile() method'); Hoek.assert(!this.realm.validator, 'Validator already set'); - this.realm.validator = Validation.validator(validator); + this.realm.validator = validator; + + return this; } start() { diff --git a/lib/types/route.d.ts b/lib/types/route.d.ts index 173964163..a0d7687c6 100644 --- a/lib/types/route.d.ts +++ b/lib/types/route.d.ts @@ -1,9 +1,7 @@ -import { ObjectSchema, ValidationOptions, SchemaMap, Schema } from 'joi'; - import { PluginSpecificConfiguration} from './plugin'; import { MergeType, ReqRef, ReqRefDefaults, MergeRefs, AuthMode } from './request'; -import { ContentDecoders, ContentEncoders, RouteRequestExtType, RouteExtObject, Server } from './server'; +import { ContentDecoders, ContentEncoders, RouteRequestExtType, RouteExtObject, Server, Validation, ServerApplicationState } from './server'; import { Lifecycle, Json, HTTP_METHODS } from './utils'; /** @@ -363,8 +361,6 @@ export interface RouteOptionsPreObject { failAction?: Lifecycle.FailAction | undefined; } -export type ValidationObject = SchemaMap; - /** * * true - any query parameter value allowed (no validation performed). false - no parameter value allowed. * * a joi validation object. @@ -372,17 +368,17 @@ export type ValidationObject = SchemaMap; * * * value - the request.* object containing the request parameters. * * * options - options. */ -export type RouteOptionsResponseSchema = - boolean - | ValidationObject - | Schema - | ((value: object | Buffer | string, options: ValidationOptions) => Promise); +export type RouteOptionsSchema = + boolean + | Validation.ExtractedSchema + | Validation.Validator + | Validation.DirectValidator; /** * Processing rules for the outgoing response. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsresponse) */ -export interface RouteOptionsResponse { +export interface RouteOptionsResponse { /** * @default 204. * The default HTTP status code when the payload is considered empty. Value can be 200 or 204. Note that a 200 status code is converted to a 204 only at the time of response transmission (the @@ -411,7 +407,7 @@ export interface RouteOptionsResponse { * custom validation function is defined via schema or status then options can an arbitrary object that will be passed to this function as the second argument. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsresponseoptions) */ - options?: ValidationOptions | undefined; // TODO needs validation + options?: Validation.ExtractedOptions | undefined; /** * @default true. @@ -440,7 +436,7 @@ export interface RouteOptionsResponse { * output.payload. If an error is thrown, the error is processed according to failAction. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsresponseschema) */ - schema?: RouteOptionsResponseSchema | undefined; + schema?: RouteOptionsSchema | undefined; /** * @default none. @@ -448,7 +444,7 @@ export interface RouteOptionsResponse { * status is set to an object where each key is a 3 digit HTTP status code and the value has the same definition as schema. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsresponsestatus) */ - status?: Record | undefined; + status?: Record> | undefined; /** * The default HTTP status code used to set a response error when the request is closed or aborted before the @@ -558,7 +554,7 @@ export type RouteOptionsSecure = boolean | RouteOptionsSecureObject; * Request input validation rules for various request components. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsvalidate) */ -export interface RouteOptionsValidate { +export interface RouteOptionsValidate { /** * @default none. * An optional object with error fields copied into every validation error response. @@ -580,7 +576,7 @@ export interface RouteOptionsValidate { * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsvalidateheaders) * @default true */ - headers?: RouteOptionsResponseSchema | undefined; + headers?: RouteOptionsSchema | undefined; /** * An options object passed to the joi rules or the custom validation methods. Used for setting global options such as stripUnknown or abortEarly (the complete list is available here). @@ -593,7 +589,7 @@ export interface RouteOptionsValidate { * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsvalidateparams) * @default true */ - options?: ValidationOptions | object | undefined; + options?: Validation.ExtractedOptions | undefined; /** * Validation rules for incoming request path parameters, after matching the path against the route, extracting any parameters, and storing them in request.params, where: @@ -607,7 +603,7 @@ export interface RouteOptionsValidate { * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsvalidateparams) * @default true */ - params?: RouteOptionsResponseSchema | undefined; + params?: RouteOptionsSchema | undefined; /** * Validation rules for incoming request payload (request body), where: @@ -617,7 +613,7 @@ export interface RouteOptionsValidate { * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsvalidatepayload) * @default true */ - payload?: RouteOptionsResponseSchema | undefined; + payload?: RouteOptionsSchema | undefined; /** * Validation rules for incoming request URI query component (the key-value part of the URI between '?' and '#'). The query is parsed into its individual key-value pairs, decoded, and stored in @@ -628,17 +624,23 @@ export interface RouteOptionsValidate { * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsvalidatequery) * @default true */ - query?: RouteOptionsResponseSchema | undefined; + query?: RouteOptionsSchema | undefined; /** * Validation rules for incoming cookies. * The cookie header is parsed and decoded into the request.state prior to validation. * @default true */ - state?: RouteOptionsResponseSchema | undefined; + state?: RouteOptionsSchema | undefined; + + /** + * Sets a server validation module used to compile raw validation rules into validation schemas (e.g. **joi**). + * @default null + */ + validator?: V | null; } -export interface CommonRouteProperties { +export interface CommonRouteProperties { /** * Application-specific route configuration state. Should not be used by plugins which should use options.plugins[name] instead. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsapp) @@ -812,7 +814,7 @@ export interface CommonRouteProperties { * Processing rules for the outgoing response. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsresponse) */ - response?: RouteOptionsResponse | undefined; + response?: RouteOptionsResponse | undefined; /** * @default false (security headers disabled). @@ -864,7 +866,7 @@ export interface CommonRouteProperties { * Request input validation rules for various request components. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsvalidate) */ - validate?: RouteOptionsValidate | undefined; + validate?: RouteOptionsValidate | undefined; } export interface AccessScopes { @@ -884,7 +886,7 @@ export interface AuthSettings { access?: AccessSetting[] | undefined; } -export interface RouteSettings extends CommonRouteProperties { +export interface RouteSettings extends CommonRouteProperties { auth?: AuthSettings | undefined; } @@ -892,7 +894,7 @@ export interface RouteSettings extends Com * Each route can be customized to change the default behavior of the request lifecycle. * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#route-options) */ -export interface RouteOptions extends CommonRouteProperties { +export interface RouteOptions extends CommonRouteProperties { /** * Route authentication configuration. Value can be: * false to disable authentication if a default strategy is set. @@ -913,10 +915,17 @@ export interface RulesInfo { vhost: string; } -export interface RulesOptions { - validate: { - schema?: ObjectSchema['Rules']> | Record['Rules'], Schema> | undefined; - options?: ValidationOptions | undefined; +export interface JoiLikeSchema { + validate(value: unknown, options: Record): { value: any } | { error: any }; +} + +export interface RulesOptions { + validate: V extends null ? { + schema: JoiLikeSchema; + options?: Record | undefined; + } : { + schema: Validation.ExtractedSchema; + options?: Validation.ExtractedOptions | undefined; }; } @@ -941,7 +950,7 @@ type RouteDefMethods = Exclude, 'HEAD' | * * rules - route custom rules object. The object is passed to each rules processor registered with server.rules(). Cannot be used if route.options.rules is defined. * For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverrouteroute) */ -export interface ServerRoute { +export interface ServerRoute { /** * (required) the absolute path used to match incoming requests (must begin with '/'). Incoming requests are compared to the configured paths based on the server's router configuration. The path * can include named parameters enclosed in {} which will be matched against literal values in the request as described in Path parameters. For context [See @@ -971,7 +980,7 @@ export interface ServerRoute { * additional route options. The options value can be an object or a function that returns an object using the signature function(server) where server is the server the route is being added to * and this is bound to the current realm's bind option. */ - options?: RouteOptions | ((server: Server) => RouteOptions) | undefined; + options?: RouteOptions | ((server: Server) => RouteOptions) | undefined; /** * route custom rules object. The object is passed to each rules processor registered with server.rules(). Cannot be used if route.options.rules is defined. diff --git a/lib/types/server/index.d.ts b/lib/types/server/index.d.ts index f1ff8aea4..3d43eea9a 100644 --- a/lib/types/server/index.d.ts +++ b/lib/types/server/index.d.ts @@ -9,3 +9,4 @@ export * from './methods'; export * from './options'; export * from './server'; export * from './state'; +export * from './validation'; diff --git a/lib/types/server/options.d.ts b/lib/types/server/options.d.ts index 68408152a..58a95193a 100644 --- a/lib/types/server/options.d.ts +++ b/lib/types/server/options.d.ts @@ -4,9 +4,11 @@ import * as https from 'https'; import { MimosOptions } from '@hapi/mimos'; import { PluginSpecificConfiguration } from '../plugin'; +import { ReqRefDefaults } from '../request'; import { RouteOptions } from '../route'; import { CacheProvider, ServerOptionsCache } from './cache'; import { SameSitePolicy } from './state'; +import { Validation } from './validation'; export interface ServerOptionsCompression { minBytes: number; @@ -25,7 +27,7 @@ export interface ServerOptionsApp { * All options are optionals. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-server-options) */ -export interface ServerOptions { +export interface ServerOptions { /** * @default '0.0.0.0' (all available network interfaces). * Sets the hostname or IP address the server will listen on. If not configured, defaults to host if present, otherwise to all available network interfaces. Set to '127.0.0.1' or 'localhost' to @@ -194,7 +196,7 @@ export interface ServerOptions { * @default none. * A route options object used as the default configuration for every route. */ - routes?: RouteOptions | undefined; + routes?: RouteOptions | undefined; /** * Default value: diff --git a/lib/types/server/server.d.ts b/lib/types/server/server.d.ts index e53a7a1eb..1b1c7d73d 100644 --- a/lib/types/server/server.d.ts +++ b/lib/types/server/server.d.ts @@ -1,7 +1,6 @@ import * as http from 'http'; import { Stream } from 'stream'; -import { Root } from 'joi'; import { Mimos } from '@hapi/mimos'; import { @@ -52,6 +51,7 @@ import { } from './methods'; import { ServerOptions } from './options'; import { ServerState, ServerStateCookieOptions } from './state'; +import { Validation } from './validation'; /** * User-extensible type for application specific state (`server.app`). @@ -64,13 +64,13 @@ export interface ServerApplicationState { * the facilities provided by the framework. Each server supports a single connection (e.g. listen to port 80). * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#server) */ -export class Server { +export class Server { /** * Creates a new server object * @param options server configuration object. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serveroptions) */ - constructor(options?: ServerOptions); + constructor(options?: ServerOptions); /** * Provides a safe place to store server-specific run-time application data without potential conflicts with @@ -90,7 +90,7 @@ export class Server { * controlled server `initialize()`/`start()`/`stop()` methods whenever the current server methods * are called, where: */ - control(server: Server): void; + control(server: Server): void; /** * Provides access to the decorations already applied to various framework interfaces. The object must not be @@ -246,7 +246,7 @@ export class Server { * The server configuration object after defaults applied. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serversettings) */ - readonly settings: ServerOptions; + readonly settings: ServerOptions; /** * The server cookies manager. @@ -315,8 +315,8 @@ export class Server { decorate(type: 'request', property: DecorateName, method: DecorationMethod, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void; decorate(type: 'toolkit', property: DecorateName, method: (existing: ((...args: any[]) => any)) => DecorationMethod, options: {apply?: boolean | undefined, extend: true}): void; decorate(type: 'toolkit', property: DecorateName, method: DecorationMethod, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void; - decorate(type: 'server', property: DecorateName, method: (existing: ((...args: any[]) => any)) => DecorationMethod, options: {apply?: boolean | undefined, extend: true}): void; - decorate(type: 'server', property: DecorateName, method: DecorationMethod, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void; + decorate(type: 'server', property: DecorateName, method: (existing: ((...args: any[]) => any)) => DecorationMethod, options: {apply?: boolean | undefined, extend: true}): void; + decorate(type: 'server', property: DecorateName, method: DecorationMethod, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void; /** * Used within a plugin to declare a required dependency on other plugins where: @@ -330,7 +330,7 @@ export class Server { * The method does not provide version dependency which should be implemented using npm peer dependencies. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverdependencydependencies-after) */ - dependency(dependencies: Dependencies, after?: ((server: Server) => Promise) | undefined): void; + dependency(dependencies: Dependencies, after?: ((server: this) => Promise) | undefined): void; /** * Registers a custom content encoding compressor to extend the built-in support for 'gzip' and 'deflate' where: @@ -562,7 +562,7 @@ export class Server { * Note that the options object is deeply cloned (with the exception of bind which is shallowly copied) and cannot contain any values that are unsafe to perform deep copy on. * [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverrouteroute) */ - route (route: ServerRoute | ServerRoute[]): void; + route(route: ServerRoute | ServerRoute[]): void; /** * Defines a route rules processor for converting route rules object into route configuration where: @@ -585,7 +585,7 @@ export class Server { */ rules ( processor: RulesProcessor, - options?: RulesOptions | undefined + options?: RulesOptions | undefined ): void; /** @@ -633,10 +633,10 @@ export class Server { * Registers a server validation module used to compile raw validation rules into validation schemas for all routes. * The validator is only used when validation rules are not pre-compiled schemas. When a validation rules is a function or schema object, the rule is used as-is and the validator is not used. */ - validator(joi: Root): void; + validator(validator: V extends null ? Vnew : Vnew extends V ? Vnew : never): Server; } /** * Factory function to create a new server object (introduced in v17). */ -export function server(opts?: ServerOptions | undefined): Server; +export function server(opts?: ServerOptions | undefined): Server; diff --git a/lib/types/server/validation.d.ts b/lib/types/server/validation.d.ts new file mode 100644 index 000000000..254cec782 --- /dev/null +++ b/lib/types/server/validation.d.ts @@ -0,0 +1,83 @@ + +import { Request, RequestRoute } from "../request"; + +export namespace Validation { + + export type ValidatedReqProperties = 'headers' | 'params' | 'query' | 'payload' | 'state'; + + export interface Context { + headers?: Request['headers']; + params?: Request['params']; + query?: Request['query']; + payload?: Request['payload']; + state?: Request['state']; + auth: Request['auth']; + app: { + route: RequestRoute['settings']['app']; + request: Request['app']; + }; + } + + export type Value = Request['headers'] | Request['params'] | Request['query'] | Request['payload'] | Request['state'] | object | undefined; + + /** + * This is called to validate user supplied values. + * + * @param value The raw value that needs to be validated. + * @param options The validation options from the config along with a `context` object. + * + * @returns The validated, and possibly transformed, value. A returned promise will be resolved, and rejections will be treated as a validation error. + * + * @throws Any thrown value is considered a validation error. + */ + export interface DirectValidator { + (value: T extends ValidatedReqProperties ? Request[T] : unknown, options: Record & { context: Omit, NonNullable> }): any; + } + + /** + * Object that can be used to validate a `value`. + */ + export interface Validator { + + /** + * This is called to validate user supplied values. + * + * @param value The raw value that needs to be validated. + * @param options The validation options from the config along with a possible `context` object. + * + * @returns On object with the result. Either the validated, and possibly transformed, `{ value }` or an `{ error }`. + */ + validate(value: Value, options: Options & { context?: Context }): { value: any } | { error: any }; + + /** + * This is called to validate user supplied values when a promise can be returned. + * + * @param value The raw value that needs to be validated. + * @param options The validation options from the config along with a possible `context` object. + * + * @returns The validated, and possibly transformed, value. A returned promise will be resolved, and rejections will be treated as a validation error. + * + * @throws Any thrown value is considered a validation error. + */ + validateAsync?(value: Value, options: AsyncOptions & { context: Context }): Promise | any; + } + + /** + * + */ + export interface Compiler { + + /** + * Converts literal schema definition to a validator object. + */ + compile(schema: object | any[], ...args: unknown[]): Validator; + } + + export type ExtractedSchema = V extends Compiler ? Parameters[0] : never; + + export type ExtractedValidateFunc = + V extends Compiler ? ReturnType['validate'] : + never; + + export type ExtractedOptions = V extends Compiler ? Omit>[1]>, 'context'> : object; +} \ No newline at end of file diff --git a/lib/validation.js b/lib/validation.js index e94bf1349..c334ba2ff 100755 --- a/lib/validation.js +++ b/lib/validation.js @@ -8,18 +8,9 @@ const Validate = require('@hapi/validate'); const internals = {}; -exports.validator = function (validator) { +exports.compile = function (rule, compiler, realm, core) { - Hoek.assert(validator, 'Missing validator'); - Hoek.assert(typeof validator.compile === 'function', 'Invalid validator compile method'); - - return validator; -}; - - -exports.compile = function (rule, validator, realm, core) { - - validator = validator ?? internals.validator(realm, core); + compiler = compiler ?? internals.validator(realm, core); // false - nothing allowed @@ -47,8 +38,11 @@ exports.compile = function (rule, validator, realm, core) { return rule; } - Hoek.assert(validator, 'Cannot set uncompiled validation rules without configuring a validator'); - return validator.compile(rule); + Hoek.assert(compiler, 'Cannot set uncompiled validation rules without configuring a validator'); + const validator = compiler.compile(rule); + + Hoek.assert(typeof validator?.validate === 'function', 'Compilation result is not a valid validator. Missing validate() method'); + return validator; }; @@ -246,5 +240,10 @@ internals.validate = function (value, schema, options) { return schema.validateAsync(value, options); } - return schema.validate(value, options); + const result = schema.validate(value, options); + if (result.error) { + throw result.error; + } + + return result; }; diff --git a/test/validation.js b/test/validation.js index b4e363ff0..0b6bea5af 100755 --- a/test/validation.js +++ b/test/validation.js @@ -25,7 +25,7 @@ describe('validation', () => { server.route({ method: 'POST', path: '/', - handler: () => 'ok', + handler: (request) => request.payload, options: { validate: { payload: JoiLegacy.object({ @@ -38,6 +38,7 @@ describe('validation', () => { const res1 = await server.inject({ url: '/', method: 'POST', payload: { a: '1', b: [1] } }); expect(res1.statusCode).to.equal(200); + expect(res1.result).to.equal({ a: 1, b: [1] }); const res2 = await server.inject({ url: '/', method: 'POST', payload: { a: 'x', b: [1] } }); expect(res2.statusCode).to.equal(400);