Skip to content

Commit

Permalink
fix: 🐛 typings around server.decorate
Browse files Browse the repository at this point in the history
Types match the validation a bit more. Type errors with error when
attempting to decorate a builtin.
  • Loading branch information
damusix committed Jan 19, 2025
1 parent 26390f2 commit 97e8fe0
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 31 deletions.
8 changes: 4 additions & 4 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ internals.Server = class {

const existing = this._core.decorations[type].get(property);
if (options.extend) {
Hoek.assert(type !== 'handler', 'Cannot extent handler decoration:', propertyName);
Hoek.assert(type !== 'handler', 'Cannot extend handler decoration:', propertyName);
Hoek.assert(existing, `Cannot extend missing ${type} decoration: ${propertyName}`);
Hoek.assert(typeof method === 'function', `Extended ${type} decoration method must be a function: ${propertyName}`);

Expand All @@ -142,7 +142,7 @@ internals.Server = class {

// Request

Hoek.assert(!this._core.Request.reserved.includes(property), 'Cannot override built-in request interface decoration:', propertyName);
Hoek.assert(!this._core.Request.reserved.includes(property), 'Cannot override the built-in request interface decoration:', propertyName);

if (options.apply) {
this._core.decorations.requestApply = this._core.decorations.requestApply ?? new Map();
Expand All @@ -156,14 +156,14 @@ internals.Server = class {

// Response

Hoek.assert(!this._core.Response.reserved.includes(property), 'Cannot override built-in response interface decoration:', propertyName);
Hoek.assert(!this._core.Response.reserved.includes(property), 'Cannot override the built-in response interface decoration:', propertyName);
this._core.Response.prototype[property] = method;
}
else if (type === 'toolkit') {

// Toolkit

Hoek.assert(!Toolkit.reserved.includes(property), 'Cannot override built-in toolkit decoration:', propertyName);
Hoek.assert(!Toolkit.reserved.includes(property), 'Cannot override the built-in toolkit decoration:', propertyName);
this._core.toolkit.decorate(property, method);
}
else {
Expand Down
7 changes: 0 additions & 7 deletions lib/types/plugin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,16 +247,9 @@ export interface HandlerDecorationMethod {
defaults?: RouteOptions | ((method: any) => RouteOptions) | undefined;
}

/**
* The general case for decorators added via server.decorate.
*/
export type DecorationMethod<T> = (this: T, ...args: any[]) => any;

/**
* An empty interface to allow typings of custom plugin properties.
*/

export interface PluginProperties {
}

export type DecorateName = string | symbol;
86 changes: 69 additions & 17 deletions lib/types/server/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import {
ServerRegisterOptions,
ServerRegisterPluginObject,
ServerRegisterPluginObjectArray,
DecorateName,
DecorationMethod,
HandlerDecorationMethod,
PluginProperties
} from '../plugin';
Expand Down Expand Up @@ -53,6 +51,57 @@ import {
import { ServerOptions } from './options';
import { ServerState, ServerStateCookieOptions } from './state';

/**
* The general case for decorators added via server.decorate.
*/
export type DecorationMethod<T> = (this: T, ...args: any[]) => any;

export type DecorateName = string | symbol;

export type DecorationValue = object | any[] | boolean | number | string | symbol | Map<any, any> | Set<any>;

type ReservedRequestKeys = (
'server' | 'url' | 'query' | 'path' | 'method' |
'mime' | 'setUrl' | 'setMethod' | 'headers' | 'id' |
'app' | 'plugins' | 'route' | 'auth' | 'pre' |
'preResponses' | 'info' | 'isInjected' | 'orig' |
'params' | 'paramsArray' | 'payload' | 'state' |
'response' | 'raw' | 'domain' | 'log' | 'logs' |
'generateResponse' |

// Private functions
'_allowInternals' | '_closed' | '_core' |
'_entity' | '_eventContext' | '_events' | '_expectContinue' |
'_isInjected' | '_isPayloadPending' | '_isReplied' |
'_route' | '_serverTimeoutId' | '_states' | '_url' |
'_urlError' | '_initializeUrl' | '_setUrl' | '_parseUrl' |
'_parseQuery'

);

type ReservedToolkitKeys = (
'abandon' | 'authenticated' | 'close' | 'context' | 'continue' |
'entity' | 'redirect' | 'realm' | 'request' | 'response' |
'state' | 'unauthenticated' | 'unstate'
);

type ReservedServerKeys = (
// Public functions
'app' | 'auth' | 'cache' | 'decorations' | 'events' | 'info' |
'listener' | 'load' | 'methods' | 'mime' | 'plugins' | 'registrations' |
'settings' | 'states' | 'type' | 'version' | 'realm' | 'control' | 'decoder' |
'bind' | 'control' | 'decoder' | 'decorate' | 'dependency' | 'encoder' |
'event' | 'expose' | 'ext' | 'inject' | 'log' | 'lookup' | 'match' | 'method' |
'path' | 'register' | 'route' | 'rules' | 'state' | 'table' | 'validator' |
'start' | 'initialize' | 'stop' |

// Private functions
'_core' | '_initialize' | '_start' | '_stop' | '_cachePolicy' | '_createCache' |
'_clone' | '_ext' | '_addRoute'
);

type ExceptName<Property, ReservedKeys> = Property extends ReservedKeys ? never : Property;

/**
* User-extensible type for application specific state (`server.app`).
*/
Expand Down Expand Up @@ -309,21 +358,24 @@ export class Server<A = ServerApplicationState> {
* @return void;
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverdecoratetype-property-method-options)
*/
decorate(type: 'handler', property: DecorateName, method: HandlerDecorationMethod, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate(type: 'request', property: DecorateName, method: (existing: ((...args: any[]) => any)) => (request: Request) => DecorationMethod<Request>, options: {apply: true, extend: true}): void;
decorate(type: 'request', property: DecorateName, method: (request: Request) => DecorationMethod<Request>, options: {apply: true, extend?: boolean | undefined}): void;
decorate(type: 'request', property: DecorateName, method: DecorationMethod<Request>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate(type: 'request', property: DecorateName, value: (existing: ((...args: any[]) => any)) => (request: Request) => any, options: {apply: true, extend: true}): void;
decorate(type: 'request', property: DecorateName, value: (request: Request) => any, options: {apply: true, extend?: boolean | undefined}): void;
decorate(type: 'request', property: DecorateName, value: any, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate(type: 'toolkit', property: DecorateName, method: (existing: ((...args: any[]) => any)) => DecorationMethod<ResponseToolkit>, options: {apply?: boolean | undefined, extend: true}): void;
decorate(type: 'toolkit', property: DecorateName, method: DecorationMethod<ResponseToolkit>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate(type: 'toolkit', property: DecorateName, value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
decorate(type: 'toolkit', property: DecorateName, value: any, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate(type: 'server', property: DecorateName, method: (existing: ((...args: any[]) => any)) => DecorationMethod<Server>, options: {apply?: boolean | undefined, extend: true}): void;
decorate(type: 'server', property: DecorateName, method: DecorationMethod<Server>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate(type: 'server', property: DecorateName, value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
decorate(type: 'server', property: DecorateName, value: any, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate <P extends DecorateName>(type: 'handler', property: P, method: HandlerDecorationMethod, options?: { apply?: boolean | undefined, extend?: never }): void;

decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, method: (existing: ((...args: any[]) => any)) => (request: Request) => DecorationMethod<Request>, options: {apply: true, extend: true}): void;
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, method: (request: Request) => DecorationMethod<Request>, options: {apply: true, extend?: boolean | undefined}): void;
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, method: DecorationMethod<Request>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, value: (existing: ((...args: any[]) => any)) => (request: Request) => any, options: {apply: true, extend: true}): void;
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, value: (request: Request) => any, options: {apply: true, extend?: boolean | undefined}): void;
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, value: DecorationValue, options?: never): void;

decorate <P extends DecorateName>(type: 'toolkit', property: ExceptName<P, ReservedToolkitKeys>, method: (existing: ((...args: any[]) => any)) => DecorationMethod<ResponseToolkit>, options: {apply?: boolean | undefined, extend: true}): void;
decorate <P extends DecorateName>(type: 'toolkit', property: ExceptName<P, ReservedToolkitKeys>, method: DecorationMethod<ResponseToolkit>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate <P extends DecorateName>(type: 'toolkit', property: ExceptName<P, ReservedToolkitKeys>, value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
decorate <P extends DecorateName>(type: 'toolkit', property: ExceptName<P, ReservedToolkitKeys>, value: DecorationValue, options?: never): void;

decorate <P extends DecorateName>(type: 'server', property: ExceptName<P, ReservedServerKeys>, method: (existing: ((...args: any[]) => any)) => DecorationMethod<Server>, options: {apply?: boolean | undefined, extend: true}): void;
decorate <P extends DecorateName>(type: 'server', property: ExceptName<P, ReservedServerKeys>, method: DecorationMethod<Server>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
decorate <P extends DecorateName>(type: 'server', property: ExceptName<P, ReservedServerKeys>, value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
decorate <P extends DecorateName>(type: 'server', property: ExceptName<P, ReservedServerKeys>, value: DecorationValue, options?: never): void;

/**
* Used within a plugin to declare a required dependency on other plugins where:
Expand Down
2 changes: 1 addition & 1 deletion test/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ describe('Server', () => {
expect(() => {

server.decorate('toolkit', 'redirect', () => { });
}).to.throw('Cannot override built-in toolkit decoration: redirect');
}).to.throw('Cannot override the built-in toolkit decoration: redirect');
});

it('decorates server', async () => {
Expand Down
95 changes: 93 additions & 2 deletions test/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
Server,
ServerRoute,
server as createServer,
ServerRegisterPluginObject
ServerRegisterPluginObject,
Lifecycle
} from '../..';

const { expect: check } = lab;
Expand Down Expand Up @@ -123,4 +124,94 @@ server.method('test.add', (a: number, b: number) => a + b, {
segment: 'test-segment',
},
generateKey: (a: number, b: number) => `${a}${b}`
});
});

declare module '../..' {
interface Request {
obj1: {
func1(a: number, b: number): number;
};

func2: (a: number, b: number) => number;
}

interface ResponseToolkit {
obj2: {
func3(a: number, b: number): number;
};

func4: (a: number, b: number) => number;
}

interface Server {
obj3: {
func5(a: number, b: number): number;
};

func6: (a: number, b: number) => number;
}
}

const theFunc = (a: number, b: number) => a + b;
const theLifecycleMethod: Lifecycle.Method = () => 'ok';

// Error when decorating existing properties
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('request', 'payload', theFunc));
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('toolkit', 'state', theFunc));
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('server', 'dependency', theFunc));
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('server', 'dependency', theFunc));

server.decorate('handler', 'func1_1', () => theLifecycleMethod);
server.decorate('handler', 'func1_2', () => theLifecycleMethod, { apply: true });

// Error when extending on handler
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('handler', 'func1_3', () => theLifecycleMethod, { apply: true, extend: true }));

// Error when handler does not return a lifecycle method
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('handler', 'func1_4', theFunc));

// Decorating request with functions
server.decorate('request', 'func2_1', theFunc);
server.decorate('request', 'func2_1', () => theFunc, { apply: true, extend: true });
server.decorate('request', 'func2_2', theFunc, { apply: true });
server.decorate('request', 'func2_2', theFunc, { extend: true });

// Decorating toolkit with functions
server.decorate('toolkit', 'func4_1', theFunc);
server.decorate('toolkit', 'func4_1', theFunc, { apply: true, extend: true });
server.decorate('toolkit', 'func4_2', theFunc, { apply: true });
server.decorate('toolkit', 'func4_2', theFunc, { extend: true });

// Decorating server with functions
server.decorate('server', 'func6_1', theFunc);
server.decorate('server', 'func6_1', theFunc, { apply: true, extend: true });
server.decorate('server', 'func6_2', theFunc, { apply: true });
server.decorate('server', 'func6_2', theFunc, { extend: true });

// Decorating request with objects
server.decorate('request', 'obj1_1', { func1: theFunc });

// Type error when extending on request with objects
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('request', 'obj1_1', { func1: theFunc }, { apply: true, extend: true }));


// Decorating toolkit with objects
server.decorate('toolkit', 'obj2_1', { func3: theFunc });

// Error when extending on toolkit with objects
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('toolkit', 'obj2_1', { func3: theFunc }, { apply: true, extend: true }));

// Decorating server with objects
server.decorate('server', 'obj3_1', { func5: theFunc });

// Error when extending on server with objects
// @ts-expect-error Lab does not support overload errors
check.error(() => server.decorate('server', 'obj3_1', { func5: theFunc }, { apply: true, extend: true }));

0 comments on commit 97e8fe0

Please sign in to comment.