Skip to content

Commit 97e8fe0

Browse files
committed
fix: 🐛 typings around server.decorate
Types match the validation a bit more. Type errors with error when attempting to decorate a builtin.
1 parent 26390f2 commit 97e8fe0

File tree

5 files changed

+167
-31
lines changed

5 files changed

+167
-31
lines changed

lib/server.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ internals.Server = class {
120120

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

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

143143
// Request
144144

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

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

157157
// Response
158158

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

164164
// Toolkit
165165

166-
Hoek.assert(!Toolkit.reserved.includes(property), 'Cannot override built-in toolkit decoration:', propertyName);
166+
Hoek.assert(!Toolkit.reserved.includes(property), 'Cannot override the built-in toolkit decoration:', propertyName);
167167
this._core.toolkit.decorate(property, method);
168168
}
169169
else {

lib/types/plugin.d.ts

-7
Original file line numberDiff line numberDiff line change
@@ -247,16 +247,9 @@ export interface HandlerDecorationMethod {
247247
defaults?: RouteOptions | ((method: any) => RouteOptions) | undefined;
248248
}
249249

250-
/**
251-
* The general case for decorators added via server.decorate.
252-
*/
253-
export type DecorationMethod<T> = (this: T, ...args: any[]) => any;
254-
255250
/**
256251
* An empty interface to allow typings of custom plugin properties.
257252
*/
258253

259254
export interface PluginProperties {
260255
}
261-
262-
export type DecorateName = string | symbol;

lib/types/server/server.d.ts

+69-17
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import {
1212
ServerRegisterOptions,
1313
ServerRegisterPluginObject,
1414
ServerRegisterPluginObjectArray,
15-
DecorateName,
16-
DecorationMethod,
1715
HandlerDecorationMethod,
1816
PluginProperties
1917
} from '../plugin';
@@ -53,6 +51,57 @@ import {
5351
import { ServerOptions } from './options';
5452
import { ServerState, ServerStateCookieOptions } from './state';
5553

54+
/**
55+
* The general case for decorators added via server.decorate.
56+
*/
57+
export type DecorationMethod<T> = (this: T, ...args: any[]) => any;
58+
59+
export type DecorateName = string | symbol;
60+
61+
export type DecorationValue = object | any[] | boolean | number | string | symbol | Map<any, any> | Set<any>;
62+
63+
type ReservedRequestKeys = (
64+
'server' | 'url' | 'query' | 'path' | 'method' |
65+
'mime' | 'setUrl' | 'setMethod' | 'headers' | 'id' |
66+
'app' | 'plugins' | 'route' | 'auth' | 'pre' |
67+
'preResponses' | 'info' | 'isInjected' | 'orig' |
68+
'params' | 'paramsArray' | 'payload' | 'state' |
69+
'response' | 'raw' | 'domain' | 'log' | 'logs' |
70+
'generateResponse' |
71+
72+
// Private functions
73+
'_allowInternals' | '_closed' | '_core' |
74+
'_entity' | '_eventContext' | '_events' | '_expectContinue' |
75+
'_isInjected' | '_isPayloadPending' | '_isReplied' |
76+
'_route' | '_serverTimeoutId' | '_states' | '_url' |
77+
'_urlError' | '_initializeUrl' | '_setUrl' | '_parseUrl' |
78+
'_parseQuery'
79+
80+
);
81+
82+
type ReservedToolkitKeys = (
83+
'abandon' | 'authenticated' | 'close' | 'context' | 'continue' |
84+
'entity' | 'redirect' | 'realm' | 'request' | 'response' |
85+
'state' | 'unauthenticated' | 'unstate'
86+
);
87+
88+
type ReservedServerKeys = (
89+
// Public functions
90+
'app' | 'auth' | 'cache' | 'decorations' | 'events' | 'info' |
91+
'listener' | 'load' | 'methods' | 'mime' | 'plugins' | 'registrations' |
92+
'settings' | 'states' | 'type' | 'version' | 'realm' | 'control' | 'decoder' |
93+
'bind' | 'control' | 'decoder' | 'decorate' | 'dependency' | 'encoder' |
94+
'event' | 'expose' | 'ext' | 'inject' | 'log' | 'lookup' | 'match' | 'method' |
95+
'path' | 'register' | 'route' | 'rules' | 'state' | 'table' | 'validator' |
96+
'start' | 'initialize' | 'stop' |
97+
98+
// Private functions
99+
'_core' | '_initialize' | '_start' | '_stop' | '_cachePolicy' | '_createCache' |
100+
'_clone' | '_ext' | '_addRoute'
101+
);
102+
103+
type ExceptName<Property, ReservedKeys> = Property extends ReservedKeys ? never : Property;
104+
56105
/**
57106
* User-extensible type for application specific state (`server.app`).
58107
*/
@@ -309,21 +358,24 @@ export class Server<A = ServerApplicationState> {
309358
* @return void;
310359
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverdecoratetype-property-method-options)
311360
*/
312-
decorate(type: 'handler', property: DecorateName, method: HandlerDecorationMethod, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
313-
decorate(type: 'request', property: DecorateName, method: (existing: ((...args: any[]) => any)) => (request: Request) => DecorationMethod<Request>, options: {apply: true, extend: true}): void;
314-
decorate(type: 'request', property: DecorateName, method: (request: Request) => DecorationMethod<Request>, options: {apply: true, extend?: boolean | undefined}): void;
315-
decorate(type: 'request', property: DecorateName, method: DecorationMethod<Request>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
316-
decorate(type: 'request', property: DecorateName, value: (existing: ((...args: any[]) => any)) => (request: Request) => any, options: {apply: true, extend: true}): void;
317-
decorate(type: 'request', property: DecorateName, value: (request: Request) => any, options: {apply: true, extend?: boolean | undefined}): void;
318-
decorate(type: 'request', property: DecorateName, value: any, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
319-
decorate(type: 'toolkit', property: DecorateName, method: (existing: ((...args: any[]) => any)) => DecorationMethod<ResponseToolkit>, options: {apply?: boolean | undefined, extend: true}): void;
320-
decorate(type: 'toolkit', property: DecorateName, method: DecorationMethod<ResponseToolkit>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
321-
decorate(type: 'toolkit', property: DecorateName, value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
322-
decorate(type: 'toolkit', property: DecorateName, value: any, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
323-
decorate(type: 'server', property: DecorateName, method: (existing: ((...args: any[]) => any)) => DecorationMethod<Server>, options: {apply?: boolean | undefined, extend: true}): void;
324-
decorate(type: 'server', property: DecorateName, method: DecorationMethod<Server>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
325-
decorate(type: 'server', property: DecorateName, value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
326-
decorate(type: 'server', property: DecorateName, value: any, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
361+
decorate <P extends DecorateName>(type: 'handler', property: P, method: HandlerDecorationMethod, options?: { apply?: boolean | undefined, extend?: never }): void;
362+
363+
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;
364+
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, method: (request: Request) => DecorationMethod<Request>, options: {apply: true, extend?: boolean | undefined}): void;
365+
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, method: DecorationMethod<Request>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
366+
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, value: (existing: ((...args: any[]) => any)) => (request: Request) => any, options: {apply: true, extend: true}): void;
367+
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, value: (request: Request) => any, options: {apply: true, extend?: boolean | undefined}): void;
368+
decorate <P extends DecorateName>(type: 'request', property: ExceptName<P, ReservedRequestKeys>, value: DecorationValue, options?: never): void;
369+
370+
decorate <P extends DecorateName>(type: 'toolkit', property: ExceptName<P, ReservedToolkitKeys>, method: (existing: ((...args: any[]) => any)) => DecorationMethod<ResponseToolkit>, options: {apply?: boolean | undefined, extend: true}): void;
371+
decorate <P extends DecorateName>(type: 'toolkit', property: ExceptName<P, ReservedToolkitKeys>, method: DecorationMethod<ResponseToolkit>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
372+
decorate <P extends DecorateName>(type: 'toolkit', property: ExceptName<P, ReservedToolkitKeys>, value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
373+
decorate <P extends DecorateName>(type: 'toolkit', property: ExceptName<P, ReservedToolkitKeys>, value: DecorationValue, options?: never): void;
374+
375+
decorate <P extends DecorateName>(type: 'server', property: ExceptName<P, ReservedServerKeys>, method: (existing: ((...args: any[]) => any)) => DecorationMethod<Server>, options: {apply?: boolean | undefined, extend: true}): void;
376+
decorate <P extends DecorateName>(type: 'server', property: ExceptName<P, ReservedServerKeys>, method: DecorationMethod<Server>, options?: {apply?: boolean | undefined, extend?: boolean | undefined}): void;
377+
decorate <P extends DecorateName>(type: 'server', property: ExceptName<P, ReservedServerKeys>, value: (existing: ((...args: any[]) => any)) => any, options: {apply?: boolean | undefined, extend: true}): void;
378+
decorate <P extends DecorateName>(type: 'server', property: ExceptName<P, ReservedServerKeys>, value: DecorationValue, options?: never): void;
327379

328380
/**
329381
* Used within a plugin to declare a required dependency on other plugins where:

test/server.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ describe('Server', () => {
592592
expect(() => {
593593

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

598598
it('decorates server', async () => {

test/types/index.ts

+93-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
Server,
1111
ServerRoute,
1212
server as createServer,
13-
ServerRegisterPluginObject
13+
ServerRegisterPluginObject,
14+
Lifecycle
1415
} from '../..';
1516

1617
const { expect: check } = lab;
@@ -123,4 +124,94 @@ server.method('test.add', (a: number, b: number) => a + b, {
123124
segment: 'test-segment',
124125
},
125126
generateKey: (a: number, b: number) => `${a}${b}`
126-
});
127+
});
128+
129+
declare module '../..' {
130+
interface Request {
131+
obj1: {
132+
func1(a: number, b: number): number;
133+
};
134+
135+
func2: (a: number, b: number) => number;
136+
}
137+
138+
interface ResponseToolkit {
139+
obj2: {
140+
func3(a: number, b: number): number;
141+
};
142+
143+
func4: (a: number, b: number) => number;
144+
}
145+
146+
interface Server {
147+
obj3: {
148+
func5(a: number, b: number): number;
149+
};
150+
151+
func6: (a: number, b: number) => number;
152+
}
153+
}
154+
155+
const theFunc = (a: number, b: number) => a + b;
156+
const theLifecycleMethod: Lifecycle.Method = () => 'ok';
157+
158+
// Error when decorating existing properties
159+
// @ts-expect-error Lab does not support overload errors
160+
check.error(() => server.decorate('request', 'payload', theFunc));
161+
// @ts-expect-error Lab does not support overload errors
162+
check.error(() => server.decorate('toolkit', 'state', theFunc));
163+
// @ts-expect-error Lab does not support overload errors
164+
check.error(() => server.decorate('server', 'dependency', theFunc));
165+
// @ts-expect-error Lab does not support overload errors
166+
check.error(() => server.decorate('server', 'dependency', theFunc));
167+
168+
server.decorate('handler', 'func1_1', () => theLifecycleMethod);
169+
server.decorate('handler', 'func1_2', () => theLifecycleMethod, { apply: true });
170+
171+
// Error when extending on handler
172+
// @ts-expect-error Lab does not support overload errors
173+
check.error(() => server.decorate('handler', 'func1_3', () => theLifecycleMethod, { apply: true, extend: true }));
174+
175+
// Error when handler does not return a lifecycle method
176+
// @ts-expect-error Lab does not support overload errors
177+
check.error(() => server.decorate('handler', 'func1_4', theFunc));
178+
179+
// Decorating request with functions
180+
server.decorate('request', 'func2_1', theFunc);
181+
server.decorate('request', 'func2_1', () => theFunc, { apply: true, extend: true });
182+
server.decorate('request', 'func2_2', theFunc, { apply: true });
183+
server.decorate('request', 'func2_2', theFunc, { extend: true });
184+
185+
// Decorating toolkit with functions
186+
server.decorate('toolkit', 'func4_1', theFunc);
187+
server.decorate('toolkit', 'func4_1', theFunc, { apply: true, extend: true });
188+
server.decorate('toolkit', 'func4_2', theFunc, { apply: true });
189+
server.decorate('toolkit', 'func4_2', theFunc, { extend: true });
190+
191+
// Decorating server with functions
192+
server.decorate('server', 'func6_1', theFunc);
193+
server.decorate('server', 'func6_1', theFunc, { apply: true, extend: true });
194+
server.decorate('server', 'func6_2', theFunc, { apply: true });
195+
server.decorate('server', 'func6_2', theFunc, { extend: true });
196+
197+
// Decorating request with objects
198+
server.decorate('request', 'obj1_1', { func1: theFunc });
199+
200+
// Type error when extending on request with objects
201+
// @ts-expect-error Lab does not support overload errors
202+
check.error(() => server.decorate('request', 'obj1_1', { func1: theFunc }, { apply: true, extend: true }));
203+
204+
205+
// Decorating toolkit with objects
206+
server.decorate('toolkit', 'obj2_1', { func3: theFunc });
207+
208+
// Error when extending on toolkit with objects
209+
// @ts-expect-error Lab does not support overload errors
210+
check.error(() => server.decorate('toolkit', 'obj2_1', { func3: theFunc }, { apply: true, extend: true }));
211+
212+
// Decorating server with objects
213+
server.decorate('server', 'obj3_1', { func5: theFunc });
214+
215+
// Error when extending on server with objects
216+
// @ts-expect-error Lab does not support overload errors
217+
check.error(() => server.decorate('server', 'obj3_1', { func5: theFunc }, { apply: true, extend: true }));

0 commit comments

Comments
 (0)