@rest-vir/run-service 0.0.2
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/LICENSE-CC0 +121 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/handle-request/endpoint-handler.d.ts +76 -0
- package/dist/handle-request/endpoint-handler.js +29 -0
- package/dist/handle-request/handle-cors.d.ts +37 -0
- package/dist/handle-request/handle-cors.js +141 -0
- package/dist/handle-request/handle-endpoint.d.ts +13 -0
- package/dist/handle-request/handle-endpoint.js +62 -0
- package/dist/handle-request/handle-request-method.d.ts +26 -0
- package/dist/handle-request/handle-request-method.js +33 -0
- package/dist/handle-request/handle-route.d.ts +15 -0
- package/dist/handle-request/handle-route.js +65 -0
- package/dist/handle-request/handle-search-params.d.ts +33 -0
- package/dist/handle-request/handle-search-params.js +32 -0
- package/dist/handle-request/handle-web-socket.d.ts +15 -0
- package/dist/handle-request/handle-web-socket.js +65 -0
- package/dist/handle-request/pre-handler.d.ts +18 -0
- package/dist/handle-request/pre-handler.js +123 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/start-service/attach-service.d.ts +45 -0
- package/dist/start-service/attach-service.js +124 -0
- package/dist/start-service/start-service-options.d.ts +83 -0
- package/dist/start-service/start-service-options.js +73 -0
- package/dist/start-service/start-service.d.ts +63 -0
- package/dist/start-service/start-service.js +85 -0
- package/dist/util/debug.d.ts +11 -0
- package/dist/util/debug.js +16 -0
- package/dist/util/headers.d.ts +11 -0
- package/dist/util/headers.js +19 -0
- package/package.json +72 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ErrorHttpStatusCategories, type HttpStatusByCategory, type SelectFrom } from '@augment-vir/common';
|
|
2
|
+
import type { BaseSearchParams } from '@rest-vir/define-service';
|
|
3
|
+
import { type ImplementedEndpoint, type ImplementedWebSocket, type ServerRequest } from '@rest-vir/implement-service';
|
|
4
|
+
/**
|
|
5
|
+
* Handles a request's search params and compares it against the route's required search params
|
|
6
|
+
* shape, if it has any.
|
|
7
|
+
*
|
|
8
|
+
* @category Internal
|
|
9
|
+
* @category Package : @rest-vir/run-service
|
|
10
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
11
|
+
*/
|
|
12
|
+
export declare function handleSearchParams({ request, route, }: Readonly<{
|
|
13
|
+
request: Readonly<Pick<ServerRequest, 'originalUrl'>>;
|
|
14
|
+
route: Readonly<SelectFrom<ImplementedEndpoint | ImplementedWebSocket, {
|
|
15
|
+
searchParamsShape: true;
|
|
16
|
+
service: {
|
|
17
|
+
logger: true;
|
|
18
|
+
serviceName: true;
|
|
19
|
+
};
|
|
20
|
+
path: true;
|
|
21
|
+
isEndpoint: true;
|
|
22
|
+
isWebSocket: true;
|
|
23
|
+
}>>;
|
|
24
|
+
}>): {
|
|
25
|
+
body?: string;
|
|
26
|
+
/**
|
|
27
|
+
* If this is set, then the response is sent with this status code and the given body (if
|
|
28
|
+
* any).
|
|
29
|
+
*/
|
|
30
|
+
statusCode: HttpStatusByCategory<ErrorHttpStatusCategories>;
|
|
31
|
+
} | {
|
|
32
|
+
data: BaseSearchParams;
|
|
33
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { HttpStatus, stringify, wrapInTry, } from '@augment-vir/common';
|
|
2
|
+
import { RestVirHandlerError, } from '@rest-vir/implement-service';
|
|
3
|
+
import { assertValidShape } from 'object-shape-tester';
|
|
4
|
+
import { parseUrl } from 'url-vir';
|
|
5
|
+
/**
|
|
6
|
+
* Handles a request's search params and compares it against the route's required search params
|
|
7
|
+
* shape, if it has any.
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
* @category Package : @rest-vir/run-service
|
|
11
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
12
|
+
*/
|
|
13
|
+
export function handleSearchParams({ request, route, }) {
|
|
14
|
+
const searchParams = parseUrl(request.originalUrl).searchParams;
|
|
15
|
+
const shape = route.searchParamsShape;
|
|
16
|
+
const validationError = shape
|
|
17
|
+
? wrapInTry(() => {
|
|
18
|
+
assertValidShape(searchParams, shape, { allowExtraKeys: true });
|
|
19
|
+
return undefined;
|
|
20
|
+
})
|
|
21
|
+
: undefined;
|
|
22
|
+
if (validationError) {
|
|
23
|
+
route.service.logger.error(new RestVirHandlerError(route, `Search params failed for ${stringify(searchParams)}: ${validationError.message}`));
|
|
24
|
+
return {
|
|
25
|
+
body: 'Invalid search params.',
|
|
26
|
+
statusCode: HttpStatus.BadRequest,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
data: searchParams,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ImplementedWebSocket, ServerRequest } from '@rest-vir/implement-service';
|
|
2
|
+
import { type WebSocket as WsWebSocket } from 'ws';
|
|
3
|
+
/**
|
|
4
|
+
* Handles a WebSocket request.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
* @category Package : @rest-vir/run-service
|
|
8
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleWebSocketRequest(this: void, { attachId, request, implementedWebSocket, webSocket: wsWebSocket, }: Readonly<{
|
|
11
|
+
request: ServerRequest;
|
|
12
|
+
attachId: string;
|
|
13
|
+
implementedWebSocket: Readonly<ImplementedWebSocket>;
|
|
14
|
+
webSocket: WsWebSocket;
|
|
15
|
+
}>): Promise<void>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { assert } from '@augment-vir/assert';
|
|
2
|
+
import { ensureErrorClass, extractErrorMessage, stringify } from '@augment-vir/common';
|
|
3
|
+
import { overwriteWebSocketMethods, parseJsonWithUndefined, WebSocketLocation, } from '@rest-vir/define-service';
|
|
4
|
+
import { RestVirHandlerError, } from '@rest-vir/implement-service';
|
|
5
|
+
import { assertValidShape } from 'object-shape-tester';
|
|
6
|
+
/**
|
|
7
|
+
* Handles a WebSocket request.
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
* @category Package : @rest-vir/run-service
|
|
11
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
12
|
+
*/
|
|
13
|
+
export async function handleWebSocketRequest({ attachId, request, implementedWebSocket, webSocket: wsWebSocket, }) {
|
|
14
|
+
const restVirContext = request.restVirContext?.[attachId];
|
|
15
|
+
assert.isDefined(restVirContext, 'restVirContext is not defined');
|
|
16
|
+
const webSocket = overwriteWebSocketMethods(implementedWebSocket, wsWebSocket, WebSocketLocation.OnHost);
|
|
17
|
+
const webSocketCallbackParams = {
|
|
18
|
+
context: restVirContext.context,
|
|
19
|
+
headers: request.headers,
|
|
20
|
+
log: implementedWebSocket.service.logger,
|
|
21
|
+
request,
|
|
22
|
+
service: implementedWebSocket.service,
|
|
23
|
+
webSocketDefinition: implementedWebSocket,
|
|
24
|
+
webSocket,
|
|
25
|
+
protocols: restVirContext.protocols,
|
|
26
|
+
searchParams: restVirContext.searchParams,
|
|
27
|
+
};
|
|
28
|
+
if (implementedWebSocket.implementation.onClose) {
|
|
29
|
+
webSocket.on('close', async () => {
|
|
30
|
+
await implementedWebSocket.implementation.onClose?.(webSocketCallbackParams);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (implementedWebSocket.implementation.onMessage) {
|
|
34
|
+
webSocket.on('message', async (rawMessage) => {
|
|
35
|
+
let message;
|
|
36
|
+
try {
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
38
|
+
const stringRawMessage = String(rawMessage);
|
|
39
|
+
message = parseJsonWithUndefined(stringRawMessage);
|
|
40
|
+
if (implementedWebSocket.messageFromClientShape) {
|
|
41
|
+
assertValidShape(message, implementedWebSocket.messageFromClientShape, { allowExtraKeys: true }, 'Invalid message send shape.');
|
|
42
|
+
}
|
|
43
|
+
else if (message) {
|
|
44
|
+
throw new Error(`Did not expect any data from the client but got ${stringify(message)}.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const errorMessage = `Failed to receive WebSocket message '${String(rawMessage)}': ${extractErrorMessage(error)}`;
|
|
49
|
+
implementedWebSocket.service.logger.error(ensureErrorClass(error, RestVirHandlerError, implementedWebSocket, errorMessage));
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await implementedWebSocket.implementation.onMessage?.({
|
|
53
|
+
...webSocketCallbackParams,
|
|
54
|
+
message,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
implementedWebSocket.service.logger.error(ensureErrorClass(error, RestVirHandlerError, implementedWebSocket, `Failed to handle WebSocket message '${String(rawMessage)}': ${extractErrorMessage(error)}`));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (implementedWebSocket.implementation.onConnection) {
|
|
63
|
+
await implementedWebSocket.implementation.onConnection(webSocketCallbackParams);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SelectFrom } from '@augment-vir/common';
|
|
2
|
+
import { GenericServiceImplementation, ServerRequest, ServerResponse } from '@rest-vir/implement-service';
|
|
3
|
+
/**
|
|
4
|
+
* Handles a request before it gets to the actual route handlers.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
* @category Package : @rest-vir/run-service
|
|
8
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
9
|
+
*/
|
|
10
|
+
export declare function preHandler(request: ServerRequest, response: ServerResponse, service: Readonly<SelectFrom<GenericServiceImplementation, {
|
|
11
|
+
webSockets: true;
|
|
12
|
+
endpoints: true;
|
|
13
|
+
serviceName: true;
|
|
14
|
+
createContext: true;
|
|
15
|
+
serviceOrigin: true;
|
|
16
|
+
requiredClientOrigin: true;
|
|
17
|
+
logger: true;
|
|
18
|
+
}>>, attachId: string): Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { assertWrap } from '@augment-vir/assert';
|
|
2
|
+
import { ensureErrorAndPrependMessage, extractErrorMessage, HttpStatus, stringify, wrapInTry, } from '@augment-vir/common';
|
|
3
|
+
import { restVirServiceNameHeader, } from '@rest-vir/define-service';
|
|
4
|
+
import { matchUrlToService } from '@rest-vir/define-service/src/service/match-url.js';
|
|
5
|
+
import { HttpMethod, RestVirHandlerError, } from '@rest-vir/implement-service';
|
|
6
|
+
import { assertValidShape, isValidShape } from 'object-shape-tester';
|
|
7
|
+
import { handleHandlerResult } from './endpoint-handler.js';
|
|
8
|
+
import { handleCors } from './handle-cors.js';
|
|
9
|
+
import { handleRequestMethod } from './handle-request-method.js';
|
|
10
|
+
import { handleSearchParams } from './handle-search-params.js';
|
|
11
|
+
/**
|
|
12
|
+
* Handles a request before it gets to the actual route handlers.
|
|
13
|
+
*
|
|
14
|
+
* @category Internal
|
|
15
|
+
* @category Package : @rest-vir/run-service
|
|
16
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
17
|
+
*/
|
|
18
|
+
export async function preHandler(request, response, service, attachId) {
|
|
19
|
+
response.header(restVirServiceNameHeader, service.serviceName);
|
|
20
|
+
const pathMatch = matchUrlToService(service, request.originalUrl);
|
|
21
|
+
if (!pathMatch) {
|
|
22
|
+
/** Nothing to do. */
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const endpointDefinition = pathMatch.endpointPath
|
|
26
|
+
? service.endpoints[pathMatch.endpointPath]
|
|
27
|
+
: undefined;
|
|
28
|
+
const webSocketDefinition = request.ws && pathMatch.webSocketPath
|
|
29
|
+
? service.webSockets[pathMatch.webSocketPath]
|
|
30
|
+
: undefined;
|
|
31
|
+
const route = endpointDefinition || webSocketDefinition;
|
|
32
|
+
if (!route) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const protocols = webSocketDefinition
|
|
36
|
+
? (request.headers['sec-websocket-protocol'] || '').split(', ')
|
|
37
|
+
: [];
|
|
38
|
+
const protocolShapeError = webSocketDefinition?.protocolsShape
|
|
39
|
+
? wrapInTry(() => assertValidShape(protocols, webSocketDefinition.protocolsShape, {
|
|
40
|
+
allowExtraKeys: true,
|
|
41
|
+
}))
|
|
42
|
+
: undefined;
|
|
43
|
+
if (protocolShapeError) {
|
|
44
|
+
service.logger.error(new RestVirHandlerError(route, extractErrorMessage(ensureErrorAndPrependMessage(protocolShapeError, `WebSocket protocols rejected (${stringify(protocols)}):`))));
|
|
45
|
+
response.statusCode = HttpStatus.BadRequest;
|
|
46
|
+
response.send('Invalid protocols.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (handleHandlerResult(await handleCors({
|
|
50
|
+
request,
|
|
51
|
+
route,
|
|
52
|
+
}), response).responseSent ||
|
|
53
|
+
handleHandlerResult(handleRequestMethod({
|
|
54
|
+
request,
|
|
55
|
+
route,
|
|
56
|
+
}), response).responseSent) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const requestData = wrapInTry(() => extractRequestData(request.body, route));
|
|
60
|
+
if (requestData instanceof Error) {
|
|
61
|
+
service.logger.error(new RestVirHandlerError(route, `Rejected request body from '${request.originalUrl}': ${stringify(requestData)}`));
|
|
62
|
+
response.statusCode = HttpStatus.BadRequest;
|
|
63
|
+
response.send('Invalid body.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const contextParams = {
|
|
67
|
+
method: assertWrap.isEnumValue(request.method.toUpperCase(), HttpMethod),
|
|
68
|
+
request,
|
|
69
|
+
requestData,
|
|
70
|
+
requestHeaders: request.headers,
|
|
71
|
+
response,
|
|
72
|
+
service,
|
|
73
|
+
endpointDefinition: endpointDefinition,
|
|
74
|
+
webSocketDefinition: webSocketDefinition,
|
|
75
|
+
};
|
|
76
|
+
const searchParams = handleSearchParams({ request, route });
|
|
77
|
+
if (!('data' in searchParams)) {
|
|
78
|
+
handleHandlerResult(searchParams, response);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const contextOutput = await service.createContext?.(contextParams);
|
|
83
|
+
if (contextOutput?.reject) {
|
|
84
|
+
service.logger.error(new RestVirHandlerError(route, `Context creation rejected: '${request.originalUrl}'`));
|
|
85
|
+
handleHandlerResult({
|
|
86
|
+
body: contextOutput.reject.responseErrorMessage,
|
|
87
|
+
statusCode: contextOutput.reject.statusCode,
|
|
88
|
+
headers: contextOutput.reject.headers,
|
|
89
|
+
}, response);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!request.restVirContext) {
|
|
93
|
+
request.restVirContext = {};
|
|
94
|
+
}
|
|
95
|
+
request.restVirContext[attachId] = {
|
|
96
|
+
context: contextOutput?.context,
|
|
97
|
+
requestData,
|
|
98
|
+
protocols,
|
|
99
|
+
searchParams: searchParams.data,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw ensureErrorAndPrependMessage(error, 'Failed to generate request context.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function extractRequestData(body, route) {
|
|
107
|
+
const dataShape = 'requestDataShape' in route ? route.requestDataShape : undefined;
|
|
108
|
+
if (!dataShape) {
|
|
109
|
+
if (body) {
|
|
110
|
+
throw new Error(`Did not expect any request data but received it.`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!isValidShape(body, dataShape, {
|
|
117
|
+
/** Allow extra keys for forwards / backwards compatibility. */
|
|
118
|
+
allowExtraKeys: true,
|
|
119
|
+
})) {
|
|
120
|
+
throw new Error('Invalid request data.');
|
|
121
|
+
}
|
|
122
|
+
return body;
|
|
123
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { HttpMethod, HttpStatus } from '@augment-vir/common';
|
|
2
|
+
export { type ServerRequest, type ServerResponse } from '@rest-vir/implement-service';
|
|
3
|
+
export * from './handle-request/endpoint-handler.js';
|
|
4
|
+
export * from './handle-request/handle-cors.js';
|
|
5
|
+
export * from './handle-request/handle-endpoint.js';
|
|
6
|
+
export * from './handle-request/handle-request-method.js';
|
|
7
|
+
export * from './handle-request/handle-route.js';
|
|
8
|
+
export * from './handle-request/handle-search-params.js';
|
|
9
|
+
export * from './handle-request/handle-web-socket.js';
|
|
10
|
+
export * from './handle-request/pre-handler.js';
|
|
11
|
+
export * from './start-service/attach-service.js';
|
|
12
|
+
export * from './start-service/start-service-options.js';
|
|
13
|
+
export * from './start-service/start-service.js';
|
|
14
|
+
export * from './test/test-endpoint.js';
|
|
15
|
+
export * from './test/test-service.js';
|
|
16
|
+
export * from './test/test-web-socket.js';
|
|
17
|
+
export * from './util/debug.js';
|
|
18
|
+
export * from './util/headers.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { HttpMethod, HttpStatus } from '@augment-vir/common';
|
|
2
|
+
export * from './handle-request/endpoint-handler.js';
|
|
3
|
+
export * from './handle-request/handle-cors.js';
|
|
4
|
+
export * from './handle-request/handle-endpoint.js';
|
|
5
|
+
export * from './handle-request/handle-request-method.js';
|
|
6
|
+
export * from './handle-request/handle-route.js';
|
|
7
|
+
export * from './handle-request/handle-search-params.js';
|
|
8
|
+
export * from './handle-request/handle-web-socket.js';
|
|
9
|
+
export * from './handle-request/pre-handler.js';
|
|
10
|
+
export * from './start-service/attach-service.js';
|
|
11
|
+
export * from './start-service/start-service-options.js';
|
|
12
|
+
export * from './start-service/start-service.js';
|
|
13
|
+
export * from './test/test-endpoint.js';
|
|
14
|
+
export * from './test/test-service.js';
|
|
15
|
+
export * from './test/test-web-socket.js';
|
|
16
|
+
export * from './util/debug.js';
|
|
17
|
+
export * from './util/headers.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SelectFrom } from '@augment-vir/common';
|
|
2
|
+
import type { BaseSearchParams } from '@rest-vir/define-service';
|
|
3
|
+
import { GenericServiceImplementation } from '@rest-vir/implement-service';
|
|
4
|
+
import { type FastifyInstance } from 'fastify';
|
|
5
|
+
import { HandleRouteOptions } from '../handle-request/endpoint-handler.js';
|
|
6
|
+
declare module 'fastify' {
|
|
7
|
+
interface FastifyRequest {
|
|
8
|
+
restVirContext: {
|
|
9
|
+
[AttachId in string]: {
|
|
10
|
+
context: unknown;
|
|
11
|
+
requestData: unknown;
|
|
12
|
+
protocols: string[];
|
|
13
|
+
searchParams: BaseSearchParams;
|
|
14
|
+
};
|
|
15
|
+
} | undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Attach all handlers for a {@link ServiceImplementation} to any existing Fastify server.
|
|
20
|
+
*
|
|
21
|
+
* @category Run Service
|
|
22
|
+
* @category Package : @rest-vir/run-service
|
|
23
|
+
* @example
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import fastify from 'fastify';
|
|
27
|
+
*
|
|
28
|
+
* const server = fastify();
|
|
29
|
+
*
|
|
30
|
+
* attachService(service, myServiceImplementation);
|
|
31
|
+
*
|
|
32
|
+
* await server.listen({port: 3000});
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
36
|
+
*/
|
|
37
|
+
export declare function attachService(server: Readonly<FastifyInstance>, service: Readonly<SelectFrom<GenericServiceImplementation, {
|
|
38
|
+
webSockets: true;
|
|
39
|
+
endpoints: true;
|
|
40
|
+
serviceName: true;
|
|
41
|
+
createContext: true;
|
|
42
|
+
serviceOrigin: true;
|
|
43
|
+
requiredClientOrigin: true;
|
|
44
|
+
logger: true;
|
|
45
|
+
}>>, options?: Readonly<HandleRouteOptions>): Promise<void>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ensureError, extractErrorMessage, getEnumValues, getObjectTypedKeys, HttpMethod, HttpStatus, randomString, } from '@augment-vir/common';
|
|
2
|
+
import fastifyWs from '@fastify/websocket';
|
|
3
|
+
import { RestVirHandlerError, } from '@rest-vir/implement-service';
|
|
4
|
+
import { handleRoute } from '../handle-request/handle-route.js';
|
|
5
|
+
import { preHandler } from '../handle-request/pre-handler.js';
|
|
6
|
+
const endpointFastifyMethods = getEnumValues(HttpMethod).filter((value) => {
|
|
7
|
+
return (
|
|
8
|
+
/** Fastify doesn't support the CONNECT method. */
|
|
9
|
+
value !== HttpMethod.Connect &&
|
|
10
|
+
/** We use the GET method in a separate handler so it can handle WebSocket requests as well. */
|
|
11
|
+
value !== HttpMethod.Get);
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Attach all handlers for a {@link ServiceImplementation} to any existing Fastify server.
|
|
15
|
+
*
|
|
16
|
+
* @category Run Service
|
|
17
|
+
* @category Package : @rest-vir/run-service
|
|
18
|
+
* @example
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import fastify from 'fastify';
|
|
22
|
+
*
|
|
23
|
+
* const server = fastify();
|
|
24
|
+
*
|
|
25
|
+
* attachService(service, myServiceImplementation);
|
|
26
|
+
*
|
|
27
|
+
* await server.listen({port: 3000});
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
31
|
+
*/
|
|
32
|
+
export async function attachService(server, service, options = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const attachId = randomString(32);
|
|
35
|
+
if (!server.hasRequestDecorator('restVirContext')) {
|
|
36
|
+
server.decorateRequest('restVirContext');
|
|
37
|
+
}
|
|
38
|
+
if (!server.hasRequestDecorator('ws')) {
|
|
39
|
+
await server.register(fastifyWs, {
|
|
40
|
+
/* node:coverage ignore next 14: edge case handling */
|
|
41
|
+
errorHandler(error, webSocket, request) {
|
|
42
|
+
service.logger.error(new RestVirHandlerError({
|
|
43
|
+
isEndpoint: false,
|
|
44
|
+
isWebSocket: true,
|
|
45
|
+
path: request.originalUrl,
|
|
46
|
+
service,
|
|
47
|
+
}, extractErrorMessage(error)));
|
|
48
|
+
webSocket.terminate();
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
server.addHook('preValidation', async (request, response) => {
|
|
53
|
+
try {
|
|
54
|
+
await preHandler(request, response, service, attachId);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
service.logger.error(ensureError(error));
|
|
58
|
+
if (options.throwErrorsForExternalHandling) {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
else if (!response.sent) {
|
|
62
|
+
response.statusCode = HttpStatus.InternalServerError;
|
|
63
|
+
response.send();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const allPaths = new Set([
|
|
68
|
+
...getObjectTypedKeys(service.webSockets),
|
|
69
|
+
...getObjectTypedKeys(service.endpoints),
|
|
70
|
+
]);
|
|
71
|
+
allPaths.forEach((path) => {
|
|
72
|
+
const webSocketDefinition = service.webSockets[path];
|
|
73
|
+
const endpoint = service.endpoints[path];
|
|
74
|
+
if (endpoint && webSocketDefinition) {
|
|
75
|
+
server.route({
|
|
76
|
+
method: endpointFastifyMethods,
|
|
77
|
+
url: path,
|
|
78
|
+
async handler(request, response) {
|
|
79
|
+
await handleRoute(undefined, request, response, endpoint, attachId, options);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
server.route({
|
|
83
|
+
method: HttpMethod.Get,
|
|
84
|
+
url: path,
|
|
85
|
+
async handler(request, response) {
|
|
86
|
+
await handleRoute(undefined, request, response, endpoint, attachId, options);
|
|
87
|
+
},
|
|
88
|
+
async wsHandler(webSocket, request) {
|
|
89
|
+
await handleRoute(webSocket, request, undefined, webSocketDefinition, attachId, options);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (endpoint) {
|
|
94
|
+
server.route({
|
|
95
|
+
method: [
|
|
96
|
+
...endpointFastifyMethods,
|
|
97
|
+
HttpMethod.Get,
|
|
98
|
+
],
|
|
99
|
+
url: path,
|
|
100
|
+
async handler(request, response) {
|
|
101
|
+
await handleRoute(undefined, request, response, endpoint, attachId, options);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else if (webSocketDefinition) {
|
|
106
|
+
server.route({
|
|
107
|
+
method: HttpMethod.Get,
|
|
108
|
+
url: path,
|
|
109
|
+
handler(request, response) {
|
|
110
|
+
response.status(HttpStatus.NotFound).send();
|
|
111
|
+
},
|
|
112
|
+
async wsHandler(webSocket, request) {
|
|
113
|
+
await handleRoute(webSocket, request, undefined, webSocketDefinition, attachId, options);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
/* node:coverage ignore next 4: this is just here to cover edge cases. */
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
service.logger.error(ensureError(error));
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
/**
|
|
3
|
+
* Shape definition for `startService` options.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
* @category Package : @rest-vir/run-service
|
|
7
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
8
|
+
*/
|
|
9
|
+
export declare const startServiceOptionsShape: import("object-shape-tester").ShapeDefinition<{
|
|
10
|
+
/**
|
|
11
|
+
* Prevent automatically choosing an available port if the provided port is already in use. This
|
|
12
|
+
* will cause `startService` to simply crash if the given port is in use.
|
|
13
|
+
*
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
lockPort: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The port that the service should listen to requests on. Note that if `lockPort` is not set,
|
|
19
|
+
* `startService` will try to find the first available port _starting_ with this given `port`
|
|
20
|
+
* property (so the actual server may be listening to a different port).
|
|
21
|
+
*
|
|
22
|
+
* If this property is set to `false`, no port will be listened to (so you can manually do that
|
|
23
|
+
* later if you wish).
|
|
24
|
+
*
|
|
25
|
+
* @default 3000
|
|
26
|
+
*/
|
|
27
|
+
port: number;
|
|
28
|
+
/**
|
|
29
|
+
* The number of workers to split the server into (for parallel request handling).
|
|
30
|
+
*
|
|
31
|
+
* @default cpus().length - 1
|
|
32
|
+
*/
|
|
33
|
+
workerCount: number;
|
|
34
|
+
/**
|
|
35
|
+
* The host name that the server should listen to. In most cases this doesn't need to be set.
|
|
36
|
+
*
|
|
37
|
+
* @default 'localhost'
|
|
38
|
+
*/
|
|
39
|
+
host: string;
|
|
40
|
+
/**
|
|
41
|
+
* If set to `true`, a multi-threaded service (`workerCount` > 1) will not automatically respawn
|
|
42
|
+
* its workers. This has no effect on single-threaded services (`workerCount` == 1).
|
|
43
|
+
*
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
preventWorkerRespawn: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Set this to true to enable temporary extra logging. This should only be used in dev as it
|
|
49
|
+
* will fill up production log files if you have a decent amount of traffic.
|
|
50
|
+
*
|
|
51
|
+
* This works by overriding the given service's logger to ensure that it logs everything.
|
|
52
|
+
*
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
debug: boolean;
|
|
56
|
+
}, false>;
|
|
57
|
+
/**
|
|
58
|
+
* Full options type for `startService`.
|
|
59
|
+
*
|
|
60
|
+
* @category Internal
|
|
61
|
+
* @category Package : @rest-vir/run-service
|
|
62
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
63
|
+
*/
|
|
64
|
+
export type StartServiceOptions = typeof startServiceOptionsShape.runtimeType;
|
|
65
|
+
/**
|
|
66
|
+
* User-provided options type for `startService`.
|
|
67
|
+
*
|
|
68
|
+
* @category Internal
|
|
69
|
+
* @category Package : @rest-vir/run-service
|
|
70
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
71
|
+
* @see {@link startServiceOptionsShape} for option explanations.
|
|
72
|
+
*/
|
|
73
|
+
export type StartServiceUserOptions = PartialWithUndefined<StartServiceOptions>;
|
|
74
|
+
/**
|
|
75
|
+
* Combines user defined options with default options to create a full options type for
|
|
76
|
+
* `startService`.
|
|
77
|
+
*
|
|
78
|
+
* @category Internal
|
|
79
|
+
* @category Package : @rest-vir/run-service
|
|
80
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
81
|
+
* @see {@link startServiceOptionsShape} for option explanations.
|
|
82
|
+
*/
|
|
83
|
+
export declare function finalizeOptions(userOptions: Readonly<StartServiceUserOptions>): StartServiceOptions;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mergeDefinedProperties } from '@augment-vir/common';
|
|
2
|
+
import { cpus } from 'node:os';
|
|
3
|
+
import { assertValidShape, defineShape } from 'object-shape-tester';
|
|
4
|
+
/**
|
|
5
|
+
* Shape definition for `startService` options.
|
|
6
|
+
*
|
|
7
|
+
* @category Internal
|
|
8
|
+
* @category Package : @rest-vir/run-service
|
|
9
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
10
|
+
*/
|
|
11
|
+
export const startServiceOptionsShape = defineShape({
|
|
12
|
+
/**
|
|
13
|
+
* Prevent automatically choosing an available port if the provided port is already in use. This
|
|
14
|
+
* will cause `startService` to simply crash if the given port is in use.
|
|
15
|
+
*
|
|
16
|
+
* @default false
|
|
17
|
+
*/
|
|
18
|
+
lockPort: false,
|
|
19
|
+
/**
|
|
20
|
+
* The port that the service should listen to requests on. Note that if `lockPort` is not set,
|
|
21
|
+
* `startService` will try to find the first available port _starting_ with this given `port`
|
|
22
|
+
* property (so the actual server may be listening to a different port).
|
|
23
|
+
*
|
|
24
|
+
* If this property is set to `false`, no port will be listened to (so you can manually do that
|
|
25
|
+
* later if you wish).
|
|
26
|
+
*
|
|
27
|
+
* @default 3000
|
|
28
|
+
*/
|
|
29
|
+
port: 3000,
|
|
30
|
+
/**
|
|
31
|
+
* The number of workers to split the server into (for parallel request handling).
|
|
32
|
+
*
|
|
33
|
+
* @default cpus().length - 1
|
|
34
|
+
*/
|
|
35
|
+
workerCount: cpus().length - 1,
|
|
36
|
+
/**
|
|
37
|
+
* The host name that the server should listen to. In most cases this doesn't need to be set.
|
|
38
|
+
*
|
|
39
|
+
* @default 'localhost'
|
|
40
|
+
*/
|
|
41
|
+
host: 'localhost',
|
|
42
|
+
/**
|
|
43
|
+
* If set to `true`, a multi-threaded service (`workerCount` > 1) will not automatically respawn
|
|
44
|
+
* its workers. This has no effect on single-threaded services (`workerCount` == 1).
|
|
45
|
+
*
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
preventWorkerRespawn: false,
|
|
49
|
+
/**
|
|
50
|
+
* Set this to true to enable temporary extra logging. This should only be used in dev as it
|
|
51
|
+
* will fill up production log files if you have a decent amount of traffic.
|
|
52
|
+
*
|
|
53
|
+
* This works by overriding the given service's logger to ensure that it logs everything.
|
|
54
|
+
*
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
debug: false,
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Combines user defined options with default options to create a full options type for
|
|
61
|
+
* `startService`.
|
|
62
|
+
*
|
|
63
|
+
* @category Internal
|
|
64
|
+
* @category Package : @rest-vir/run-service
|
|
65
|
+
* @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
|
|
66
|
+
* @see {@link startServiceOptionsShape} for option explanations.
|
|
67
|
+
*/
|
|
68
|
+
export function finalizeOptions(userOptions) {
|
|
69
|
+
const options = mergeDefinedProperties(startServiceOptionsShape.defaultValue, userOptions);
|
|
70
|
+
options.workerCount = Math.max(1, options.workerCount);
|
|
71
|
+
assertValidShape(options, startServiceOptionsShape);
|
|
72
|
+
return options;
|
|
73
|
+
}
|