@smounters/imperium 1.0.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/core/app-tokens.d.ts +5 -0
- package/dist/core/app-tokens.js +4 -0
- package/dist/core/application.d.ts +34 -0
- package/dist/core/application.js +252 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +1 -0
- package/dist/core/container.d.ts +76 -0
- package/dist/core/container.js +687 -0
- package/dist/core/errors.d.ts +31 -0
- package/dist/core/errors.js +169 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +4 -0
- package/dist/core/logger.d.ts +7 -0
- package/dist/core/logger.js +12 -0
- package/dist/core/reflector.d.ts +9 -0
- package/dist/core/reflector.js +39 -0
- package/dist/core/server.d.ts +10 -0
- package/dist/core/server.js +296 -0
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.js +1 -0
- package/dist/decorators/di.decorators.d.ts +10 -0
- package/dist/decorators/di.decorators.js +15 -0
- package/dist/decorators/filters.decorators.d.ts +6 -0
- package/dist/decorators/filters.decorators.js +16 -0
- package/dist/decorators/guards.decorators.d.ts +4 -0
- package/dist/decorators/guards.decorators.js +8 -0
- package/dist/decorators/http.decorators.d.ts +16 -0
- package/dist/decorators/http.decorators.js +60 -0
- package/dist/decorators/index.d.ts +8 -0
- package/dist/decorators/index.js +8 -0
- package/dist/decorators/interceptors.decorators.d.ts +4 -0
- package/dist/decorators/interceptors.decorators.js +8 -0
- package/dist/decorators/metadata.decorators.d.ts +4 -0
- package/dist/decorators/metadata.decorators.js +22 -0
- package/dist/decorators/pipes.decorators.d.ts +4 -0
- package/dist/decorators/pipes.decorators.js +8 -0
- package/dist/decorators/rpc.decorators.d.ts +11 -0
- package/dist/decorators/rpc.decorators.js +50 -0
- package/dist/http/adapter.d.ts +4 -0
- package/dist/http/adapter.js +199 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.js +3 -0
- package/dist/http/router-builder.d.ts +4 -0
- package/dist/http/router-builder.js +30 -0
- package/dist/http/utils.d.ts +6 -0
- package/dist/http/utils.js +46 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pipes/index.d.ts +1 -0
- package/dist/pipes/index.js +1 -0
- package/dist/pipes/zod.pipe.d.ts +7 -0
- package/dist/pipes/zod.pipe.js +8 -0
- package/dist/rpc/adapter.d.ts +7 -0
- package/dist/rpc/adapter.js +186 -0
- package/dist/rpc/index.d.ts +3 -0
- package/dist/rpc/index.js +3 -0
- package/dist/rpc/router-builder.d.ts +4 -0
- package/dist/rpc/router-builder.js +28 -0
- package/dist/rpc/utils.d.ts +6 -0
- package/dist/rpc/utils.js +46 -0
- package/dist/services/config.service.d.ts +10 -0
- package/dist/services/config.service.js +41 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +2 -0
- package/dist/services/logger.service.d.ts +30 -0
- package/dist/services/logger.service.js +52 -0
- package/dist/types.d.ts +167 -0
- package/dist/types.js +1 -0
- package/dist/validation/app-config.d.ts +30 -0
- package/dist/validation/app-config.js +25 -0
- package/dist/validation/common.d.ts +8 -0
- package/dist/validation/common.js +57 -0
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.js +2 -0
- package/package.json +98 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ConnectError } from "@connectrpc/connect";
|
|
2
|
+
export declare class HttpException extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly response?: unknown | undefined;
|
|
5
|
+
constructor(status: number, message: string, response?: unknown | undefined);
|
|
6
|
+
getResponse(): unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare class BadRequestException extends HttpException {
|
|
9
|
+
constructor(message?: string, response?: unknown);
|
|
10
|
+
}
|
|
11
|
+
export declare class UnauthorizedException extends HttpException {
|
|
12
|
+
constructor(message?: string, response?: unknown);
|
|
13
|
+
}
|
|
14
|
+
export declare class ForbiddenException extends HttpException {
|
|
15
|
+
constructor(message?: string, response?: unknown);
|
|
16
|
+
}
|
|
17
|
+
export declare class NotFoundException extends HttpException {
|
|
18
|
+
constructor(message?: string, response?: unknown);
|
|
19
|
+
}
|
|
20
|
+
export declare class InternalServerErrorException extends HttpException {
|
|
21
|
+
constructor(message?: string, response?: unknown);
|
|
22
|
+
}
|
|
23
|
+
interface ErrorMappingOptions {
|
|
24
|
+
exposeInternalErrors?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function toHttpError(error: unknown, options?: ErrorMappingOptions): {
|
|
27
|
+
status: number;
|
|
28
|
+
body: unknown;
|
|
29
|
+
};
|
|
30
|
+
export declare function toConnectError(error: unknown, options?: ErrorMappingOptions): ConnectError;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
2
|
+
export class HttpException extends Error {
|
|
3
|
+
constructor(status, message, response) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.response = response;
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
}
|
|
9
|
+
getResponse() {
|
|
10
|
+
if (this.response !== undefined) {
|
|
11
|
+
return this.response;
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
statusCode: this.status,
|
|
15
|
+
message: this.message,
|
|
16
|
+
error: this.name,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class BadRequestException extends HttpException {
|
|
21
|
+
constructor(message = "Bad Request", response) {
|
|
22
|
+
super(400, message, response);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class UnauthorizedException extends HttpException {
|
|
26
|
+
constructor(message = "Unauthorized", response) {
|
|
27
|
+
super(401, message, response);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ForbiddenException extends HttpException {
|
|
31
|
+
constructor(message = "Forbidden", response) {
|
|
32
|
+
super(403, message, response);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class NotFoundException extends HttpException {
|
|
36
|
+
constructor(message = "Not Found", response) {
|
|
37
|
+
super(404, message, response);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class InternalServerErrorException extends HttpException {
|
|
41
|
+
constructor(message = "Internal Server Error", response) {
|
|
42
|
+
super(500, message, response);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function internalErrorMessage(error, options) {
|
|
46
|
+
if (options?.exposeInternalErrors && error.message.trim().length > 0) {
|
|
47
|
+
return error.message;
|
|
48
|
+
}
|
|
49
|
+
return "Internal Server Error";
|
|
50
|
+
}
|
|
51
|
+
function connectCodeToHttpStatus(code) {
|
|
52
|
+
switch (code) {
|
|
53
|
+
case Code.Canceled:
|
|
54
|
+
return 499;
|
|
55
|
+
case Code.Unknown:
|
|
56
|
+
return 500;
|
|
57
|
+
case Code.InvalidArgument:
|
|
58
|
+
return 400;
|
|
59
|
+
case Code.DeadlineExceeded:
|
|
60
|
+
return 504;
|
|
61
|
+
case Code.NotFound:
|
|
62
|
+
return 404;
|
|
63
|
+
case Code.AlreadyExists:
|
|
64
|
+
return 409;
|
|
65
|
+
case Code.PermissionDenied:
|
|
66
|
+
return 403;
|
|
67
|
+
case Code.ResourceExhausted:
|
|
68
|
+
return 429;
|
|
69
|
+
case Code.FailedPrecondition:
|
|
70
|
+
return 412;
|
|
71
|
+
case Code.Aborted:
|
|
72
|
+
return 409;
|
|
73
|
+
case Code.OutOfRange:
|
|
74
|
+
return 400;
|
|
75
|
+
case Code.Unimplemented:
|
|
76
|
+
return 501;
|
|
77
|
+
case Code.Internal:
|
|
78
|
+
return 500;
|
|
79
|
+
case Code.Unavailable:
|
|
80
|
+
return 503;
|
|
81
|
+
case Code.DataLoss:
|
|
82
|
+
return 500;
|
|
83
|
+
case Code.Unauthenticated:
|
|
84
|
+
return 401;
|
|
85
|
+
default:
|
|
86
|
+
return 500;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function httpStatusToConnectCode(status) {
|
|
90
|
+
if (status === 400)
|
|
91
|
+
return Code.InvalidArgument;
|
|
92
|
+
if (status === 401)
|
|
93
|
+
return Code.Unauthenticated;
|
|
94
|
+
if (status === 403)
|
|
95
|
+
return Code.PermissionDenied;
|
|
96
|
+
if (status === 404)
|
|
97
|
+
return Code.NotFound;
|
|
98
|
+
if (status === 409)
|
|
99
|
+
return Code.Aborted;
|
|
100
|
+
if (status === 412)
|
|
101
|
+
return Code.FailedPrecondition;
|
|
102
|
+
if (status === 429)
|
|
103
|
+
return Code.ResourceExhausted;
|
|
104
|
+
if (status === 499)
|
|
105
|
+
return Code.Canceled;
|
|
106
|
+
if (status === 501)
|
|
107
|
+
return Code.Unimplemented;
|
|
108
|
+
if (status === 503)
|
|
109
|
+
return Code.Unavailable;
|
|
110
|
+
if (status === 504)
|
|
111
|
+
return Code.DeadlineExceeded;
|
|
112
|
+
return Code.Internal;
|
|
113
|
+
}
|
|
114
|
+
export function toHttpError(error, options) {
|
|
115
|
+
if (error instanceof HttpException) {
|
|
116
|
+
return {
|
|
117
|
+
status: error.status,
|
|
118
|
+
body: error.getResponse(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (error instanceof ConnectError) {
|
|
122
|
+
const status = connectCodeToHttpStatus(error.code);
|
|
123
|
+
const isInternal = status >= 500;
|
|
124
|
+
return {
|
|
125
|
+
status,
|
|
126
|
+
body: {
|
|
127
|
+
statusCode: status,
|
|
128
|
+
message: isInternal ? internalErrorMessage(error, options) : error.message,
|
|
129
|
+
code: error.code,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (error instanceof Error) {
|
|
134
|
+
return {
|
|
135
|
+
status: 500,
|
|
136
|
+
body: {
|
|
137
|
+
statusCode: 500,
|
|
138
|
+
message: internalErrorMessage(error, options),
|
|
139
|
+
error: error.name,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
status: 500,
|
|
145
|
+
body: {
|
|
146
|
+
statusCode: 500,
|
|
147
|
+
message: "Internal Server Error",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function toConnectError(error, options) {
|
|
152
|
+
if (error instanceof ConnectError) {
|
|
153
|
+
if (error.code === Code.Internal && !options?.exposeInternalErrors) {
|
|
154
|
+
return new ConnectError("Internal Server Error", error.code);
|
|
155
|
+
}
|
|
156
|
+
return error;
|
|
157
|
+
}
|
|
158
|
+
if (error instanceof HttpException) {
|
|
159
|
+
const code = httpStatusToConnectCode(error.status);
|
|
160
|
+
if (code === Code.Internal && !options?.exposeInternalErrors) {
|
|
161
|
+
return new ConnectError("Internal Server Error", code);
|
|
162
|
+
}
|
|
163
|
+
return new ConnectError(error.message, code);
|
|
164
|
+
}
|
|
165
|
+
if (error instanceof Error) {
|
|
166
|
+
return new ConnectError(internalErrorMessage(error, options), Code.Internal);
|
|
167
|
+
}
|
|
168
|
+
return new ConnectError("Internal Server Error", Code.Internal);
|
|
169
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Application } from "./application";
|
|
2
|
+
export { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from "./app-tokens";
|
|
3
|
+
export { BadRequestException, ForbiddenException, HttpException, InternalServerErrorException, NotFoundException, UnauthorizedException, } from "./errors";
|
|
4
|
+
export { Reflector } from "./reflector";
|
|
5
|
+
export type * from "../types";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Application } from "./application";
|
|
2
|
+
export { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from "./app-tokens";
|
|
3
|
+
export { BadRequestException, ForbiddenException, HttpException, InternalServerErrorException, NotFoundException, UnauthorizedException, } from "./errors";
|
|
4
|
+
export { Reflector } from "./reflector";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Logger } from "tslog";
|
|
2
|
+
import type { LoggerOptions } from "../types";
|
|
3
|
+
type LoggerPayload = Record<string, unknown>;
|
|
4
|
+
export type AppLogger = Logger<LoggerPayload>;
|
|
5
|
+
export declare const LOGGER_TOKEN: unique symbol;
|
|
6
|
+
export declare function createLogger(options?: LoggerOptions): AppLogger;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Logger } from "tslog";
|
|
2
|
+
export const LOGGER_TOKEN = Symbol("app:logger");
|
|
3
|
+
const DEFAULT_LOGGER_OPTIONS = {
|
|
4
|
+
name: "app",
|
|
5
|
+
type: "pretty",
|
|
6
|
+
};
|
|
7
|
+
export function createLogger(options) {
|
|
8
|
+
return new Logger({
|
|
9
|
+
...DEFAULT_LOGGER_OPTIONS,
|
|
10
|
+
...(options ?? {}),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import type { MetadataKey } from "../types";
|
|
3
|
+
type MetadataTarget = object | Function | undefined;
|
|
4
|
+
export declare class Reflector {
|
|
5
|
+
get<T = unknown>(metadataKey: MetadataKey, target: MetadataTarget): T | undefined;
|
|
6
|
+
getAllAndOverride<T = unknown>(metadataKey: MetadataKey, targets: MetadataTarget[]): T | undefined;
|
|
7
|
+
getAllAndMerge<T = unknown[]>(metadataKey: MetadataKey, targets: MetadataTarget[]): T;
|
|
8
|
+
}
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import "reflect-metadata";
|
|
8
|
+
import { Injectable } from "../decorators/di.decorators";
|
|
9
|
+
let Reflector = class Reflector {
|
|
10
|
+
get(metadataKey, target) {
|
|
11
|
+
if (!target) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return Reflect.getMetadata(metadataKey, target);
|
|
15
|
+
}
|
|
16
|
+
getAllAndOverride(metadataKey, targets) {
|
|
17
|
+
for (const target of targets) {
|
|
18
|
+
const value = this.get(metadataKey, target);
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
getAllAndMerge(metadataKey, targets) {
|
|
26
|
+
const result = [];
|
|
27
|
+
for (const target of targets) {
|
|
28
|
+
const value = this.get(metadataKey, target);
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
result.push(...value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
Reflector = __decorate([
|
|
37
|
+
Injectable()
|
|
38
|
+
], Reflector);
|
|
39
|
+
export { Reflector };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { type FastifyInstance } from "fastify";
|
|
3
|
+
import type { ServerOptions } from "../types";
|
|
4
|
+
import { AppContainer } from "./container";
|
|
5
|
+
type LegacyServerOptions = ServerOptions & {
|
|
6
|
+
di: AppContainer;
|
|
7
|
+
};
|
|
8
|
+
export declare function startServer(di: AppContainer, options: ServerOptions): Promise<FastifyInstance>;
|
|
9
|
+
export declare function startServer(options: LegacyServerOptions): Promise<FastifyInstance>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
|
|
3
|
+
import fastifyCors from "@fastify/cors";
|
|
4
|
+
import Fastify from "fastify";
|
|
5
|
+
import { HTTP_ROUTES_KEY } from "../decorators/http.decorators";
|
|
6
|
+
import { RPC_METHODS_KEY, RPC_SERVICE_KEY } from "../decorators/rpc.decorators";
|
|
7
|
+
import { registerHttpRoutes } from "../http";
|
|
8
|
+
import { buildConnectRoutes } from "../rpc";
|
|
9
|
+
import { AppContainer } from "./container";
|
|
10
|
+
function normalizePrefix(prefix) {
|
|
11
|
+
if (!prefix) {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
const trimmed = prefix.trim();
|
|
15
|
+
if (trimmed.length === 0 || trimmed === "/") {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
19
|
+
if (withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/")) {
|
|
20
|
+
return withLeadingSlash.slice(0, -1);
|
|
21
|
+
}
|
|
22
|
+
return withLeadingSlash;
|
|
23
|
+
}
|
|
24
|
+
function mergePrefixes(...prefixes) {
|
|
25
|
+
const normalized = prefixes.map((prefix) => normalizePrefix(prefix)).filter((prefix) => prefix.length > 0);
|
|
26
|
+
return normalized.join("");
|
|
27
|
+
}
|
|
28
|
+
function hasRegisteredHttpHandlers(di) {
|
|
29
|
+
for (const controller of di.getHttpControllers()) {
|
|
30
|
+
const routes = Reflect.getMetadata(HTTP_ROUTES_KEY, controller);
|
|
31
|
+
if ((routes?.length ?? 0) > 0) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function hasRegisteredRpcHandlers(di) {
|
|
38
|
+
for (const controller of di.getControllers()) {
|
|
39
|
+
const service = Reflect.getMetadata(RPC_SERVICE_KEY, controller);
|
|
40
|
+
if (!service) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const methods = Reflect.getMetadata(RPC_METHODS_KEY, controller);
|
|
44
|
+
if ((methods?.length ?? 0) > 0) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function isValidPort(port) {
|
|
51
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
52
|
+
}
|
|
53
|
+
function resolveListenPort(port, config) {
|
|
54
|
+
if (typeof port === "number" && isValidPort(port)) {
|
|
55
|
+
return port;
|
|
56
|
+
}
|
|
57
|
+
const configPort = config.APP_PORT;
|
|
58
|
+
if (typeof configPort === "number" && isValidPort(configPort)) {
|
|
59
|
+
return configPort;
|
|
60
|
+
}
|
|
61
|
+
if (typeof configPort === "string") {
|
|
62
|
+
const normalized = configPort.trim();
|
|
63
|
+
if (normalized.length > 0) {
|
|
64
|
+
const parsed = Number(normalized);
|
|
65
|
+
if (isValidPort(parsed)) {
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Server port is invalid. Provide options.port or APP_PORT in range 1..65535.");
|
|
71
|
+
}
|
|
72
|
+
function resolveCors(options) {
|
|
73
|
+
if (options === undefined || options === false) {
|
|
74
|
+
return { enabled: false, options: {} };
|
|
75
|
+
}
|
|
76
|
+
if (options === true) {
|
|
77
|
+
return { enabled: true, options: {} };
|
|
78
|
+
}
|
|
79
|
+
const { enabled = true, ...corsOptions } = options;
|
|
80
|
+
return { enabled, options: corsOptions };
|
|
81
|
+
}
|
|
82
|
+
function resolveHealth(options) {
|
|
83
|
+
const defaults = {
|
|
84
|
+
enabled: false,
|
|
85
|
+
path: "/health",
|
|
86
|
+
};
|
|
87
|
+
if (options === undefined || options === false) {
|
|
88
|
+
return defaults;
|
|
89
|
+
}
|
|
90
|
+
if (options === true) {
|
|
91
|
+
return {
|
|
92
|
+
...defaults,
|
|
93
|
+
enabled: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
enabled: options.enabled ?? true,
|
|
98
|
+
path: normalizePrefix(options.path) || defaults.path,
|
|
99
|
+
check: options.check,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeHealthResult(result) {
|
|
103
|
+
if (typeof result === "boolean") {
|
|
104
|
+
return { ok: result };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
ok: result.ok,
|
|
108
|
+
details: result.details,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function runHealthCheck(check, exposeInternalErrors, logger, path) {
|
|
112
|
+
if (!check) {
|
|
113
|
+
return { ok: true };
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const result = await check();
|
|
117
|
+
return normalizeHealthResult(result);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
logger.error({
|
|
121
|
+
type: "health_check_error",
|
|
122
|
+
path,
|
|
123
|
+
}, error);
|
|
124
|
+
if (exposeInternalErrors && error instanceof Error) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
details: {
|
|
128
|
+
message: error.message,
|
|
129
|
+
error: error.name,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { ok: false };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function startServer(diOrOptions, options) {
|
|
137
|
+
const di = diOrOptions instanceof AppContainer ? diOrOptions : diOrOptions.di;
|
|
138
|
+
const serverOptions = diOrOptions instanceof AppContainer ? options : diOrOptions;
|
|
139
|
+
if (!serverOptions) {
|
|
140
|
+
throw new Error("Server options are required");
|
|
141
|
+
}
|
|
142
|
+
const { port, host = "0.0.0.0", prefix, httpPrefix, rpcPrefix, trustProxy, requestTimeout, connectionTimeout, keepAliveTimeout, bodyLimit, routerOptions, maxParamLength: legacyMaxParamLength, pluginTimeout, accessLogs = false, exposeInternalErrors = false, cors, health, loggerOptions, config, } = serverOptions;
|
|
143
|
+
if (loggerOptions !== undefined) {
|
|
144
|
+
di.configureLogger(loggerOptions);
|
|
145
|
+
}
|
|
146
|
+
if (config) {
|
|
147
|
+
di.configureConfig(config.schema, config.source ?? process.env);
|
|
148
|
+
}
|
|
149
|
+
di.setExposeInternalErrors(exposeInternalErrors);
|
|
150
|
+
const listenPort = resolveListenPort(port, di.getConfig());
|
|
151
|
+
const healthConfig = resolveHealth(health);
|
|
152
|
+
const http = hasRegisteredHttpHandlers(di);
|
|
153
|
+
const rpc = hasRegisteredRpcHandlers(di);
|
|
154
|
+
if (!http && !rpc && !healthConfig.enabled) {
|
|
155
|
+
throw new Error("No handlers found for HTTP or RPC. Register controllers in @Module().");
|
|
156
|
+
}
|
|
157
|
+
let server;
|
|
158
|
+
try {
|
|
159
|
+
await di.init();
|
|
160
|
+
const resolvedRouterOptions = legacyMaxParamLength === undefined
|
|
161
|
+
? routerOptions
|
|
162
|
+
: {
|
|
163
|
+
...(routerOptions ?? {}),
|
|
164
|
+
maxParamLength: legacyMaxParamLength,
|
|
165
|
+
};
|
|
166
|
+
server = Fastify({
|
|
167
|
+
logger: false,
|
|
168
|
+
trustProxy,
|
|
169
|
+
requestTimeout,
|
|
170
|
+
connectionTimeout,
|
|
171
|
+
keepAliveTimeout,
|
|
172
|
+
bodyLimit,
|
|
173
|
+
pluginTimeout,
|
|
174
|
+
...(resolvedRouterOptions ? { routerOptions: resolvedRouterOptions } : {}),
|
|
175
|
+
});
|
|
176
|
+
const appLogger = di.getLogger();
|
|
177
|
+
const corsConfig = resolveCors(cors);
|
|
178
|
+
const effectiveHttpPrefix = mergePrefixes(prefix, httpPrefix);
|
|
179
|
+
const effectiveRpcPrefix = mergePrefixes(prefix, rpcPrefix);
|
|
180
|
+
server.addHook("onRequest", async (req) => {
|
|
181
|
+
const request = req;
|
|
182
|
+
request.requestStartAt = Date.now();
|
|
183
|
+
});
|
|
184
|
+
server.addHook("onResponse", async (req, reply) => {
|
|
185
|
+
const request = req;
|
|
186
|
+
const startedAt = request.requestStartAt ?? Date.now();
|
|
187
|
+
if (accessLogs) {
|
|
188
|
+
appLogger.info({
|
|
189
|
+
type: "access",
|
|
190
|
+
method: req.method,
|
|
191
|
+
url: req.url,
|
|
192
|
+
statusCode: reply.statusCode,
|
|
193
|
+
durationMs: Date.now() - startedAt,
|
|
194
|
+
ip: req.ip,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (request.diScope) {
|
|
198
|
+
try {
|
|
199
|
+
await di.disposeRequestScope(request.diScope);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
appLogger.error({
|
|
203
|
+
type: "request_scope_dispose_error",
|
|
204
|
+
method: req.method,
|
|
205
|
+
url: req.url,
|
|
206
|
+
}, error);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
request.diScope = undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
server.addHook("onRequestAbort", async (req) => {
|
|
214
|
+
const request = req;
|
|
215
|
+
if (request.diScope) {
|
|
216
|
+
try {
|
|
217
|
+
await di.disposeRequestScope(request.diScope);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
appLogger.error({
|
|
221
|
+
type: "request_scope_abort_dispose_error",
|
|
222
|
+
method: req.method,
|
|
223
|
+
url: req.url,
|
|
224
|
+
}, error);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
request.diScope = undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
server.addHook("onClose", async () => {
|
|
232
|
+
await di.close("server.close");
|
|
233
|
+
});
|
|
234
|
+
if (corsConfig.enabled) {
|
|
235
|
+
await server.register(fastifyCors, corsConfig.options);
|
|
236
|
+
}
|
|
237
|
+
if (healthConfig.enabled) {
|
|
238
|
+
const healthUrl = mergePrefixes(effectiveHttpPrefix, healthConfig.path);
|
|
239
|
+
server.get(healthUrl || "/", async (_req, reply) => {
|
|
240
|
+
const result = await runHealthCheck(healthConfig.check, exposeInternalErrors, appLogger, healthUrl || "/");
|
|
241
|
+
reply.status(result.ok ? 200 : 503).send({
|
|
242
|
+
status: result.ok ? "ok" : "error",
|
|
243
|
+
type: "health",
|
|
244
|
+
...(result.details !== undefined ? { details: result.details } : {}),
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (http) {
|
|
249
|
+
registerHttpRoutes(server, di, effectiveHttpPrefix);
|
|
250
|
+
}
|
|
251
|
+
if (rpc) {
|
|
252
|
+
await server.register(fastifyConnectPlugin, {
|
|
253
|
+
prefix: effectiveRpcPrefix,
|
|
254
|
+
routes: buildConnectRoutes(di),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
await server.listen({ port: listenPort, host });
|
|
258
|
+
return server;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const logger = (() => {
|
|
262
|
+
try {
|
|
263
|
+
return di.getLogger();
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
if (logger) {
|
|
270
|
+
logger.error({
|
|
271
|
+
type: "server_start_failed",
|
|
272
|
+
host,
|
|
273
|
+
port: listenPort,
|
|
274
|
+
}, error);
|
|
275
|
+
}
|
|
276
|
+
if (server) {
|
|
277
|
+
try {
|
|
278
|
+
await server.close();
|
|
279
|
+
}
|
|
280
|
+
catch (closeError) {
|
|
281
|
+
if (logger) {
|
|
282
|
+
logger.error({ type: "server_start_cleanup_error", stage: "server.close" }, closeError);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
await di.close("server.start_failed");
|
|
288
|
+
}
|
|
289
|
+
catch (closeError) {
|
|
290
|
+
if (logger) {
|
|
291
|
+
logger.error({ type: "server_start_cleanup_error", stage: "di.close" }, closeError);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ConnectRouter } from "@connectrpc/connect";
|
|
2
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
3
|
+
export interface HttpRouteMeta {
|
|
4
|
+
method: HttpMethod;
|
|
5
|
+
path: string;
|
|
6
|
+
handlerName: string;
|
|
7
|
+
}
|
|
8
|
+
export type HttpParamSource = "body" | "query" | "param" | "header" | "req" | "res";
|
|
9
|
+
export interface HttpParamMeta {
|
|
10
|
+
index: number;
|
|
11
|
+
source: HttpParamSource;
|
|
12
|
+
key?: string;
|
|
13
|
+
}
|
|
14
|
+
export type RpcParamSource = "data" | "context" | "headers" | "header";
|
|
15
|
+
export interface RpcParamMeta {
|
|
16
|
+
index: number;
|
|
17
|
+
source: RpcParamSource;
|
|
18
|
+
key?: string;
|
|
19
|
+
}
|
|
20
|
+
export type RpcServiceDescriptor = Parameters<ConnectRouter["service"]>[0];
|
|
21
|
+
export type RpcMethodDescriptor = Parameters<ConnectRouter["rpc"]>[0];
|
|
22
|
+
export interface RpcMethodMeta {
|
|
23
|
+
method: RpcMethodDescriptor;
|
|
24
|
+
handlerName: string;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { inject, injectAll, injectable, Lifecycle } from "tsyringe";
|
|
3
|
+
import type { InjectionToken, ModuleMeta } from "../types";
|
|
4
|
+
export declare const MODULE_KEY: unique symbol;
|
|
5
|
+
export declare function Module(meta: ModuleMeta): ClassDecorator;
|
|
6
|
+
export declare const Injectable: typeof injectable;
|
|
7
|
+
export declare const Inject: typeof inject;
|
|
8
|
+
export declare const InjectAll: typeof injectAll;
|
|
9
|
+
export declare const Scope: typeof Lifecycle;
|
|
10
|
+
export declare function Optional(token: InjectionToken<unknown>): ParameterDecorator;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { inject, injectAll, injectable, Lifecycle } from "tsyringe";
|
|
3
|
+
export const MODULE_KEY = Symbol("module");
|
|
4
|
+
export function Module(meta) {
|
|
5
|
+
return (target) => {
|
|
6
|
+
Reflect.defineMetadata(MODULE_KEY, meta, target);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export const Injectable = injectable;
|
|
10
|
+
export const Inject = inject;
|
|
11
|
+
export const InjectAll = injectAll;
|
|
12
|
+
export const Scope = Lifecycle;
|
|
13
|
+
export function Optional(token) {
|
|
14
|
+
return inject(token, { isOptional: true });
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import type { Constructor, ExceptionFilterLike } from "../types";
|
|
3
|
+
export declare const FILTERS_KEY: unique symbol;
|
|
4
|
+
export declare const CATCH_EXCEPTIONS_KEY: unique symbol;
|
|
5
|
+
export declare function UseFilters(...filters: ExceptionFilterLike[]): ClassDecorator & MethodDecorator;
|
|
6
|
+
export declare function Catch(...exceptions: Constructor[]): ClassDecorator;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { Injectable } from "./di.decorators";
|
|
3
|
+
import { appendArrayMetadata } from "./metadata.decorators";
|
|
4
|
+
export const FILTERS_KEY = Symbol("filters");
|
|
5
|
+
export const CATCH_EXCEPTIONS_KEY = Symbol("filter:exceptions");
|
|
6
|
+
export function UseFilters(...filters) {
|
|
7
|
+
return (target, propertyKey) => {
|
|
8
|
+
appendArrayMetadata(FILTERS_KEY, filters, target, propertyKey);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function Catch(...exceptions) {
|
|
12
|
+
return (target) => {
|
|
13
|
+
Injectable()(target);
|
|
14
|
+
Reflect.defineMetadata(CATCH_EXCEPTIONS_KEY, exceptions, target);
|
|
15
|
+
};
|
|
16
|
+
}
|