@grest-ts/http 0.0.5
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 +21 -0
- package/README.md +613 -0
- package/dist/src/client/GGHttpSchema.createClient.d.ts +14 -0
- package/dist/src/client/GGHttpSchema.createClient.d.ts.map +1 -0
- package/dist/src/client/GGHttpSchema.createClient.js +80 -0
- package/dist/src/client/GGHttpSchema.createClient.js.map +1 -0
- package/dist/src/index-browser.d.ts +8 -0
- package/dist/src/index-browser.d.ts.map +1 -0
- package/dist/src/index-browser.js +11 -0
- package/dist/src/index-browser.js.map +1 -0
- package/dist/src/index-node.d.ts +18 -0
- package/dist/src/index-node.d.ts.map +1 -0
- package/dist/src/index-node.js +32 -0
- package/dist/src/index-node.js.map +1 -0
- package/dist/src/rpc/GGHttpRouteRPC.d.ts +19 -0
- package/dist/src/rpc/GGHttpRouteRPC.d.ts.map +1 -0
- package/dist/src/rpc/GGHttpRouteRPC.js +32 -0
- package/dist/src/rpc/GGHttpRouteRPC.js.map +1 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts +18 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts.map +1 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js +80 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js.map +1 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts +18 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts.map +1 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js +90 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js.map +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts +12 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts.map +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js +77 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js.map +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseParser.d.ts +7 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseParser.d.ts.map +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseParser.js +21 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseParser.js.map +1 -0
- package/dist/src/schema/GGHttpSchema.d.ts +68 -0
- package/dist/src/schema/GGHttpSchema.d.ts.map +1 -0
- package/dist/src/schema/GGHttpSchema.js +18 -0
- package/dist/src/schema/GGHttpSchema.js.map +1 -0
- package/dist/src/schema/httpSchema.d.ts +43 -0
- package/dist/src/schema/httpSchema.d.ts.map +1 -0
- package/dist/src/schema/httpSchema.js +85 -0
- package/dist/src/schema/httpSchema.js.map +1 -0
- package/dist/src/server/GGHttp.d.ts +12 -0
- package/dist/src/server/GGHttp.d.ts.map +1 -0
- package/dist/src/server/GGHttp.js +16 -0
- package/dist/src/server/GGHttp.js.map +1 -0
- package/dist/src/server/GGHttpMetrics.d.ts +22 -0
- package/dist/src/server/GGHttpMetrics.d.ts.map +1 -0
- package/dist/src/server/GGHttpMetrics.js +15 -0
- package/dist/src/server/GGHttpMetrics.js.map +1 -0
- package/dist/src/server/GGHttpSchema.startServer.d.ts +30 -0
- package/dist/src/server/GGHttpSchema.startServer.d.ts.map +1 -0
- package/dist/src/server/GGHttpSchema.startServer.js +114 -0
- package/dist/src/server/GGHttpSchema.startServer.js.map +1 -0
- package/dist/src/server/GGHttpServer.d.ts +32 -0
- package/dist/src/server/GGHttpServer.d.ts.map +1 -0
- package/dist/src/server/GGHttpServer.js +116 -0
- package/dist/src/server/GGHttpServer.js.map +1 -0
- package/dist/src/server/GG_HTTP_REQUEST.d.ts +16 -0
- package/dist/src/server/GG_HTTP_REQUEST.d.ts.map +1 -0
- package/dist/src/server/GG_HTTP_REQUEST.js +10 -0
- package/dist/src/server/GG_HTTP_REQUEST.js.map +1 -0
- package/dist/src/server/GG_HTTP_SERVER.d.ts +4 -0
- package/dist/src/server/GG_HTTP_SERVER.d.ts.map +1 -0
- package/dist/src/server/GG_HTTP_SERVER.js +3 -0
- package/dist/src/server/GG_HTTP_SERVER.js.map +1 -0
- package/dist/src/tsconfig.json +17 -0
- package/dist/testkit/clientHttp/GGHttpCall.d.ts +35 -0
- package/dist/testkit/clientHttp/GGHttpCall.d.ts.map +1 -0
- package/dist/testkit/clientHttp/GGHttpCall.js +37 -0
- package/dist/testkit/clientHttp/GGHttpCall.js.map +1 -0
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.d.ts +37 -0
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.d.ts.map +1 -0
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.js +29 -0
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.js.map +1 -0
- package/dist/testkit/index-testkit.d.ts +8 -0
- package/dist/testkit/index-testkit.d.ts.map +1 -0
- package/dist/testkit/index-testkit.js +8 -0
- package/dist/testkit/index-testkit.js.map +1 -0
- package/dist/testkit/mock/GGHttpInterceptorsServer.d.ts +13 -0
- package/dist/testkit/mock/GGHttpInterceptorsServer.d.ts.map +1 -0
- package/dist/testkit/mock/GGHttpInterceptorsServer.js +100 -0
- package/dist/testkit/mock/GGHttpInterceptorsServer.js.map +1 -0
- package/dist/testkit/mock/GGHttpSchema.mock.d.ts +36 -0
- package/dist/testkit/mock/GGHttpSchema.mock.d.ts.map +1 -0
- package/dist/testkit/mock/GGHttpSchema.mock.js +78 -0
- package/dist/testkit/mock/GGHttpSchema.mock.js.map +1 -0
- package/dist/testkit/routing/GGApiRoutingSelector.d.ts +8 -0
- package/dist/testkit/routing/GGApiRoutingSelector.d.ts.map +1 -0
- package/dist/testkit/routing/GGApiRoutingSelector.js +4 -0
- package/dist/testkit/routing/GGApiRoutingSelector.js.map +1 -0
- package/dist/testkit/routing/GGHttpSchema.routing.d.ts +14 -0
- package/dist/testkit/routing/GGHttpSchema.routing.d.ts.map +1 -0
- package/dist/testkit/routing/GGHttpSchema.routing.js +21 -0
- package/dist/testkit/routing/GGHttpSchema.routing.js.map +1 -0
- package/dist/testkit/utils/validateContractResponse.d.ts +8 -0
- package/dist/testkit/utils/validateContractResponse.d.ts.map +1 -0
- package/dist/testkit/utils/validateContractResponse.js +68 -0
- package/dist/testkit/utils/validateContractResponse.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/package.json +74 -0
- package/src/client/GGHttpSchema.createClient.ts +107 -0
- package/src/index-browser.ts +12 -0
- package/src/index-node.ts +38 -0
- package/src/rpc/GGHttpRouteRPC.ts +42 -0
- package/src/rpc/RpcRequest/GGRpcRequestBuilder.ts +91 -0
- package/src/rpc/RpcRequest/GGRpcRequestParser.ts +100 -0
- package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +84 -0
- package/src/rpc/RpcResponse/GGRpcResponseParser.ts +23 -0
- package/src/schema/GGHttpSchema.ts +115 -0
- package/src/schema/httpSchema.ts +99 -0
- package/src/server/GGHttp.ts +27 -0
- package/src/server/GGHttpMetrics.ts +31 -0
- package/src/server/GGHttpSchema.startServer.ts +161 -0
- package/src/server/GGHttpServer.ts +133 -0
- package/src/server/GG_HTTP_REQUEST.ts +12 -0
- package/src/server/GG_HTTP_SERVER.ts +4 -0
- package/src/tsconfig.json +17 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type {HttpMethod} from "@grest-ts/common";
|
|
2
|
+
import {GGContractMethod} from "@grest-ts/schema";
|
|
3
|
+
import {ClientHttpRouteToRpcTransformClientConfig, GGHttpFetchRequest, GGHttpTransportMiddleware} from "../../schema/GGHttpSchema";
|
|
4
|
+
|
|
5
|
+
export class GGRpcRequestBuilder {
|
|
6
|
+
|
|
7
|
+
public readonly contract: GGContractMethod
|
|
8
|
+
public readonly middlewares: readonly GGHttpTransportMiddleware[]
|
|
9
|
+
public readonly method: HttpMethod;
|
|
10
|
+
public readonly pathTemplate: string;
|
|
11
|
+
public readonly pathParams: string[];
|
|
12
|
+
public readonly pathPrefix: string;
|
|
13
|
+
public readonly hasBody: boolean;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
method: HttpMethod,
|
|
17
|
+
pathTemplate: string,
|
|
18
|
+
config: ClientHttpRouteToRpcTransformClientConfig
|
|
19
|
+
) {
|
|
20
|
+
this.method = method
|
|
21
|
+
this.pathTemplate = pathTemplate
|
|
22
|
+
this.pathPrefix = config.pathPrefix
|
|
23
|
+
this.contract = config.contract
|
|
24
|
+
this.middlewares = config.middlewares
|
|
25
|
+
this.pathParams = (pathTemplate.match(/:(\w+)/g) || []).map(m => m.slice(1))
|
|
26
|
+
this.hasBody = method === "POST" || method === "PUT" || method === "PATCH"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public createRequest = (data: unknown): GGHttpFetchRequest => {
|
|
30
|
+
let result: GGHttpFetchRequest;
|
|
31
|
+
if (this.hasBody) {
|
|
32
|
+
result = {
|
|
33
|
+
url: this.pathPrefix + this.buildPath(data),
|
|
34
|
+
method: this.method,
|
|
35
|
+
headers: {'Content-Type': 'application/json'},
|
|
36
|
+
body: this.buildBody(data)
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
result = {
|
|
40
|
+
url: this.pathPrefix + this.buildPath(data) + this.buildQueryString(data as Record<string, unknown>),
|
|
41
|
+
method: this.method,
|
|
42
|
+
headers: {},
|
|
43
|
+
body: undefined
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
this.middlewares?.forEach(mw => mw.updateRequest?.(result))
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private buildPath(data: unknown) {
|
|
51
|
+
let path: string = this.pathTemplate;
|
|
52
|
+
if (this.pathParams.length > 0 && typeof data === "object" && data) {
|
|
53
|
+
for (let i = 0; i < this.pathParams.length; i++) {
|
|
54
|
+
const p = this.pathParams[i]
|
|
55
|
+
const val = (data as Record<string, unknown>)[p]
|
|
56
|
+
path = path.replace(':' + p, encodeURIComponent(val === undefined || val === null ? "" : String(val)))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private buildBody(data: unknown) {
|
|
63
|
+
if (data === undefined || data === null) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (this.contract.input?.def.hasNonJsonData) {
|
|
67
|
+
throw new Error("Schema contains non-JSON data (e.g. files). Use GGRpc.MULTIPART_POST instead of GGRpc.POST for this route.")
|
|
68
|
+
} else {
|
|
69
|
+
return JSON.stringify(data)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private buildQueryString(data: unknown): string {
|
|
74
|
+
if (data && typeof data === "object") {
|
|
75
|
+
const params = new URLSearchParams()
|
|
76
|
+
for (const [key, value] of Object.entries(data)) {
|
|
77
|
+
if (value !== undefined && value !== null) {
|
|
78
|
+
if (this.pathParams.length > 0 && this.pathParams.includes(key)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
params.append(key, String(value))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const query = params.toString()
|
|
85
|
+
if (query) {
|
|
86
|
+
return "?" + query
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return ""
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type http from "http";
|
|
2
|
+
import type {HttpMethod} from "@grest-ts/common";
|
|
3
|
+
import {GGContractExecutor, GGContractMethod} from "@grest-ts/schema";
|
|
4
|
+
import {ClientHttpRouteToRpcTransformServerConfig, GGHttpRequest, GGHttpTransportMiddleware} from "../../schema/GGHttpSchema";
|
|
5
|
+
import type {GGHttpServerMiddleware} from "../../server/GGHttpSchema.startServer";
|
|
6
|
+
|
|
7
|
+
export class GGRpcRequestParser {
|
|
8
|
+
|
|
9
|
+
private readonly pathTemplate: string;
|
|
10
|
+
private readonly pathParams: string[];
|
|
11
|
+
private readonly hasBody: boolean;
|
|
12
|
+
protected readonly contract: GGContractMethod
|
|
13
|
+
private readonly apiMiddlewares: readonly GGHttpTransportMiddleware[]
|
|
14
|
+
private readonly serverMiddlewares: readonly GGHttpServerMiddleware[]
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
method: HttpMethod,
|
|
18
|
+
pathTemplate: string,
|
|
19
|
+
config: ClientHttpRouteToRpcTransformServerConfig
|
|
20
|
+
) {
|
|
21
|
+
this.pathTemplate = pathTemplate
|
|
22
|
+
this.pathParams = (pathTemplate.match(/:(\w+)/g) || []).map(m => m.slice(1))
|
|
23
|
+
this.hasBody = method === "POST" || method === "PUT" || method === "PATCH"
|
|
24
|
+
this.contract = config.contract
|
|
25
|
+
this.apiMiddlewares = config.apiMiddlewares
|
|
26
|
+
this.serverMiddlewares = config.serverMiddlewares
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public parseRequest = async (req: http.IncomingMessage): Promise<unknown> => {
|
|
30
|
+
const url = req.url || '/'
|
|
31
|
+
const qIndex = url.indexOf('?')
|
|
32
|
+
const queryArgs = this.parseQueryString(qIndex === -1 ? '' : url.substring(qIndex + 1))
|
|
33
|
+
if (this.apiMiddlewares?.length > 0) {
|
|
34
|
+
const mwQuery: GGHttpRequest = {headers: req.headers, queryArgs: queryArgs}
|
|
35
|
+
this.apiMiddlewares?.forEach(mw => mw.parseRequest?.(mwQuery))
|
|
36
|
+
}
|
|
37
|
+
for (const mw of this.serverMiddlewares ?? []) await mw.process?.();
|
|
38
|
+
|
|
39
|
+
let input: unknown;
|
|
40
|
+
if (this.hasBody) {
|
|
41
|
+
input = await this.parseBody(req);
|
|
42
|
+
} else if (this.pathParams.length > 0) {
|
|
43
|
+
input = {
|
|
44
|
+
...this.extractPathParams(qIndex === -1 ? url : url.substring(0, qIndex)),
|
|
45
|
+
...queryArgs
|
|
46
|
+
};
|
|
47
|
+
} else {
|
|
48
|
+
input = queryArgs;
|
|
49
|
+
}
|
|
50
|
+
return GGContractExecutor.parseInput(this.contract.input, input);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private parseQueryString(rawQuery: string): Record<string, string | string[]> {
|
|
54
|
+
const result: Record<string, string | string[]> = {}
|
|
55
|
+
if (rawQuery) {
|
|
56
|
+
const params = new URLSearchParams(rawQuery)
|
|
57
|
+
for (const [key, value] of params.entries()) {
|
|
58
|
+
result[key] = value
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private extractPathParams(actualPath: string): Record<string, string> {
|
|
65
|
+
const result: Record<string, string> = {}
|
|
66
|
+
const templateParts = this.pathTemplate.split('/').filter(p => p)
|
|
67
|
+
const actualParts = actualPath.split('/').filter(p => p)
|
|
68
|
+
const offset = actualParts.length - templateParts.length
|
|
69
|
+
if (offset >= 0) {
|
|
70
|
+
for (let i = 0; i < templateParts.length; i++) {
|
|
71
|
+
const templatePart = templateParts[i]
|
|
72
|
+
if (templatePart.startsWith(':')) {
|
|
73
|
+
const paramName = templatePart.slice(1)
|
|
74
|
+
result[paramName] = decodeURIComponent(actualParts[offset + i] || '')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async parseBody(req: http.IncomingMessage): Promise<unknown> {
|
|
82
|
+
const contentType = req.headers['content-type'] || ''
|
|
83
|
+
const isMultipart = contentType.toLowerCase().startsWith('multipart/form-data')
|
|
84
|
+
if (isMultipart) {
|
|
85
|
+
throw new Error("Received multipart request on a JSON-only route. Use GGRpc.MULTIPART_POST for routes with file uploads.")
|
|
86
|
+
} else {
|
|
87
|
+
const rawBody: Buffer = await new Promise((resolve, reject) => {
|
|
88
|
+
const chunks: Buffer[] = []
|
|
89
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
90
|
+
req.on('end', () => resolve(Buffer.concat(chunks)))
|
|
91
|
+
req.on('error', reject)
|
|
92
|
+
});
|
|
93
|
+
if (rawBody && rawBody.length > 0) {
|
|
94
|
+
return JSON.parse(rawBody.toString('utf-8'))
|
|
95
|
+
} else {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type http from "http";
|
|
2
|
+
import {ANY_ERROR, ERROR, GGContractExecutor, GGContractMethod, GGDebugData, GGErrorData, GGSchema, OK} from "@grest-ts/schema";
|
|
3
|
+
import {ClientHttpRouteToRpcTransformServerConfig} from "../../schema/GGHttpSchema";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class GGRpcResponseBuilder {
|
|
7
|
+
|
|
8
|
+
protected readonly contract: GGContractMethod
|
|
9
|
+
|
|
10
|
+
constructor(config: ClientHttpRouteToRpcTransformServerConfig) {
|
|
11
|
+
this.contract = config.contract;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public sendResponse = async (res: http.ServerResponse, rpcResult: ERROR<string, unknown> | OK<unknown>): Promise<void> => {
|
|
15
|
+
let json: string;
|
|
16
|
+
let statusCode: number;
|
|
17
|
+
if (rpcResult.success === true) {
|
|
18
|
+
statusCode = 200;
|
|
19
|
+
json = '{' +
|
|
20
|
+
'"success":true,' +
|
|
21
|
+
'"type":"OK"' +
|
|
22
|
+
this.makeDataStr(this.contract.success, rpcResult) +
|
|
23
|
+
"}"
|
|
24
|
+
} else {
|
|
25
|
+
json = this.makeError(GGContractExecutor.getResponseSchema(this.contract, rpcResult), rpcResult);
|
|
26
|
+
statusCode = rpcResult.statusCode;
|
|
27
|
+
}
|
|
28
|
+
res.writeHead(statusCode, {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'Content-Length': Buffer.byteLength(json)
|
|
31
|
+
});
|
|
32
|
+
res.end(json)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private makeError(schema: GGSchema<any> | undefined, rpcResult: ERROR<string, unknown>) {
|
|
36
|
+
return '{' +
|
|
37
|
+
'"success":false,' +
|
|
38
|
+
'"type":' + JSON.stringify(rpcResult.type) + ',' +
|
|
39
|
+
'"statusCode":' + Number(rpcResult.statusCode) +
|
|
40
|
+
this.makeErrorCtx(rpcResult.context, rpcResult.getDebugContext()) +
|
|
41
|
+
this.makeDataStr(schema, rpcResult) +
|
|
42
|
+
"}";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private makeDataStr(schema: GGSchema<any>, data: OK<any> | ANY_ERROR): string {
|
|
46
|
+
if (schema) {
|
|
47
|
+
GGContractExecutor.assertResponse(schema, data);
|
|
48
|
+
const dataStr = schema.unsafeStringify(data.data)
|
|
49
|
+
return dataStr ? ',"data":' + dataStr + "" : "";
|
|
50
|
+
} else {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private makeErrorCtx(ctx: GGErrorData, debugContext?: GGDebugData): string {
|
|
56
|
+
if (ctx) {
|
|
57
|
+
let str = "";
|
|
58
|
+
str += ctx?.displayMessage ? '"displayMessage":' + JSON.stringify(ctx.displayMessage) + '' : "";
|
|
59
|
+
str += ctx?.timestamp ? (str ? "," : "") + '"timestamp":' + Number(ctx.timestamp) : "";
|
|
60
|
+
str += ctx?.ref ? (str ? "," : "") + '"ref":' + JSON.stringify(ctx.ref) + '' : "";
|
|
61
|
+
if (process.env.NODE_ENV !== "production" && debugContext) {
|
|
62
|
+
if (debugContext?.debugMessage) {
|
|
63
|
+
str += (str ? "," : "") + '"debugMessage":' + JSON.stringify(debugContext.debugMessage);
|
|
64
|
+
}
|
|
65
|
+
if (debugContext?.debugData !== undefined) {
|
|
66
|
+
str += (str ? "," : "") + '"debugData":' + JSON.stringify(debugContext.debugData);
|
|
67
|
+
}
|
|
68
|
+
if (debugContext?.originalError) {
|
|
69
|
+
const origErr = debugContext.originalError;
|
|
70
|
+
const errInfo = origErr instanceof ERROR
|
|
71
|
+
? origErr.toJSON()
|
|
72
|
+
: origErr instanceof Error
|
|
73
|
+
? {message: origErr.message, stack: origErr.stack?.split("\n")}
|
|
74
|
+
: {message: String(origErr)};
|
|
75
|
+
str += (str ? "," : "") + '"originalError":' + JSON.stringify(errInfo);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (str) {
|
|
79
|
+
return ',"context":{' + str + '}';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {ERROR_JSON, OK, SERVER_ERROR} from "@grest-ts/schema";
|
|
2
|
+
import {ClientHttpRouteToRpcTransformClientConfig} from "../../schema/GGHttpSchema";
|
|
3
|
+
|
|
4
|
+
export class GGRpcResponseParser {
|
|
5
|
+
|
|
6
|
+
constructor(config: ClientHttpRouteToRpcTransformClientConfig) {
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
parseResponse = async (response: Response): Promise<OK<unknown> | ERROR_JSON<string, unknown>> => {
|
|
11
|
+
const txt = await response.text();
|
|
12
|
+
try {
|
|
13
|
+
const json = txt ? JSON.parse(txt) : {};
|
|
14
|
+
if (typeof json === "object" && "success" in json && "type" in json) {
|
|
15
|
+
return json
|
|
16
|
+
} else {
|
|
17
|
+
return new SERVER_ERROR({displayMessage: "Invalid response format!", debugData: {json: json}});
|
|
18
|
+
}
|
|
19
|
+
} catch (err) {
|
|
20
|
+
return new SERVER_ERROR({displayMessage: "Failed to parse JSON", originalError: err, debugData: {text: txt}});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {ERROR, ERROR_JSON, GGContractApiDefinition, GGContractClass, GGContractMethod, OK} from "@grest-ts/schema";
|
|
2
|
+
import type {HttpMethod} from "@grest-ts/common";
|
|
3
|
+
import type http from "http";
|
|
4
|
+
import type {GGHttpServerMiddleware} from "../server/GGHttpSchema.startServer";
|
|
5
|
+
|
|
6
|
+
export class GGHttpSchema<TContract extends GGContractApiDefinition, TContext> {
|
|
7
|
+
|
|
8
|
+
public readonly name: string
|
|
9
|
+
public readonly pathPrefix: string
|
|
10
|
+
public readonly apiMiddlewares: readonly GGHttpTransportMiddleware[]
|
|
11
|
+
public readonly codec: Record<keyof TContract, GGHttpCodec>
|
|
12
|
+
public readonly contract: GGContractClass<TContract> | null = null
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
pathPrefix: string,
|
|
16
|
+
contract: GGContractClass<TContract>,
|
|
17
|
+
wireCodec: Record<keyof TContract, GGHttpCodec>,
|
|
18
|
+
middlewares: readonly GGHttpTransportMiddleware[] = []
|
|
19
|
+
) {
|
|
20
|
+
this.name = contract.name
|
|
21
|
+
this.pathPrefix = pathPrefix
|
|
22
|
+
this.apiMiddlewares = middlewares
|
|
23
|
+
this.codec = wireCodec
|
|
24
|
+
this.contract = contract
|
|
25
|
+
Object.freeze(this.apiMiddlewares)
|
|
26
|
+
Object.freeze(this.codec)
|
|
27
|
+
Object.freeze(this)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --------------------------------------------------------------------------------------------------------
|
|
32
|
+
// Client codec
|
|
33
|
+
// --------------------------------------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface ClientHttpRouteToRpcTransformClientConfig {
|
|
36
|
+
pathPrefix: string,
|
|
37
|
+
contract: GGContractMethod,
|
|
38
|
+
middlewares: readonly GGHttpTransportMiddleware[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ClientHttpRouteToRpcTransformClientCodec {
|
|
42
|
+
createRequest: (data: unknown) => GGHttpFetchRequest | Promise<GGHttpFetchRequest>
|
|
43
|
+
parseResponse: (response: Response) => Promise<OK<unknown> | ERROR_JSON<string, unknown>>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GGHttpFetchRequest {
|
|
47
|
+
url: string;
|
|
48
|
+
method: HttpMethod;
|
|
49
|
+
headers: Record<string, string>;
|
|
50
|
+
body: string | FormData | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --------------------------------------------------------------------------------------------------------
|
|
54
|
+
// Server codec
|
|
55
|
+
// --------------------------------------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export interface ClientHttpRouteToRpcTransformServerConfig {
|
|
58
|
+
contract: GGContractMethod,
|
|
59
|
+
apiMiddlewares: readonly GGHttpTransportMiddleware[],
|
|
60
|
+
serverMiddlewares: readonly GGHttpServerMiddleware[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ClientHttpRouteToRpcTransformServerCodec {
|
|
64
|
+
parseRequest: (req: http.IncomingMessage) => Promise<unknown>,
|
|
65
|
+
sendResponse: (res: http.ServerResponse, rpcResult: ERROR<string, unknown> | OK<unknown>) => Promise<void>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --------------------------------------------------------------------------------------------------------
|
|
69
|
+
// Codec
|
|
70
|
+
// --------------------------------------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export interface GGHttpCodec {
|
|
73
|
+
readonly method: HttpMethod;
|
|
74
|
+
readonly path: string;
|
|
75
|
+
|
|
76
|
+
createForClient(config: ClientHttpRouteToRpcTransformClientConfig): ClientHttpRouteToRpcTransformClientCodec
|
|
77
|
+
|
|
78
|
+
createForServer(config: ClientHttpRouteToRpcTransformServerConfig): ClientHttpRouteToRpcTransformServerCodec
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --------------------------------------------------------------------------------------------------------
|
|
83
|
+
// Middleware
|
|
84
|
+
// --------------------------------------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export interface GGHttpTransportMiddleware {
|
|
87
|
+
/**
|
|
88
|
+
* Client-side: modify outgoing request (add headers, etc.)
|
|
89
|
+
*/
|
|
90
|
+
updateRequest?(req: GGHttpRequest): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Server-side: parse incoming request (extract context from headers, etc.)
|
|
94
|
+
*/
|
|
95
|
+
parseRequest?(req: GGHttpRequest): void;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Server-side: modify outgoing response
|
|
99
|
+
*/
|
|
100
|
+
updateResponse?(res: GGHttpResponse): void;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Client-side: parse incoming response
|
|
104
|
+
*/
|
|
105
|
+
parseResponse?(res: GGHttpResponse): void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface GGHttpRequest {
|
|
109
|
+
headers?: Record<string, string | string[]>;
|
|
110
|
+
queryArgs?: Record<string, string | string[]>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface GGHttpResponse {
|
|
114
|
+
headers: Record<string, string | string[]>;
|
|
115
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type http from "http";
|
|
2
|
+
import {GGHttpCodec, GGHttpSchema} from "./GGHttpSchema";
|
|
3
|
+
import {GGHttpRequest, GGHttpTransportMiddleware} from "./GGHttpSchema";
|
|
4
|
+
import {GGContractApiDefinition, GGContractClass} from "@grest-ts/schema";
|
|
5
|
+
import {GGContextKey} from "@grest-ts/context";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create an HTTP API schema builder from a contract.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* export const MyApiContract = defineApi("MyApi", () => ({
|
|
12
|
+
* list: {
|
|
13
|
+
* success: IsArray(IsItem),
|
|
14
|
+
* errors: [NOT_AUTHORIZED, SERVER_ERROR]
|
|
15
|
+
* },
|
|
16
|
+
* create: {
|
|
17
|
+
* input: IsCreateRequest,
|
|
18
|
+
* success: IsItem,
|
|
19
|
+
* errors: [NOT_AUTHORIZED, VALIDATION_ERROR, SERVER_ERROR]
|
|
20
|
+
* }
|
|
21
|
+
* }))
|
|
22
|
+
*
|
|
23
|
+
* export const MyApi = httpApi(MyApiContract)
|
|
24
|
+
* .pathPrefix("api/items")
|
|
25
|
+
* .use(AuthMiddleware)
|
|
26
|
+
* .routes({
|
|
27
|
+
* list: GGRpc.GET("list"),
|
|
28
|
+
* create: GGRpc.POST("create")
|
|
29
|
+
* })
|
|
30
|
+
*/
|
|
31
|
+
export function httpSchema<TContract extends GGContractApiDefinition>(
|
|
32
|
+
contract: GGContractClass<TContract>
|
|
33
|
+
): GGHttpSchemaBuilder<TContract> {
|
|
34
|
+
return new GGHttpSchemaBuilder(contract)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class GGHttpSchemaBuilder<TContract extends GGContractApiDefinition, TContext = undefined> {
|
|
38
|
+
|
|
39
|
+
private readonly _contract: GGContractClass<TContract>
|
|
40
|
+
private _pathPrefix: string = ""
|
|
41
|
+
private _middlewares: GGHttpTransportMiddleware[] = []
|
|
42
|
+
|
|
43
|
+
constructor(contract: GGContractClass<TContract>) {
|
|
44
|
+
this._contract = contract;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pathPrefix(prefix: string): this {
|
|
48
|
+
this._pathPrefix = prefix
|
|
49
|
+
return this
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
use<M extends GGHttpTransportMiddleware>(middleware: M): GGHttpSchemaBuilder<TContract, TContext | M> {
|
|
53
|
+
this._middlewares.push(middleware)
|
|
54
|
+
return this as unknown as GGHttpSchemaBuilder<TContract, TContext | M>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
useHeader<Input>(contextKey: GGContextKey<Input>): GGHttpSchemaBuilder<TContract, TContext | Input> {
|
|
58
|
+
const codec = contextKey.getCodec("http");
|
|
59
|
+
if (!codec) {
|
|
60
|
+
throw new Error(`Context key '${contextKey.name}' does not have an 'http-header' codec registered.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const middleware: GGHttpTransportMiddleware = {
|
|
64
|
+
updateRequest(req: GGHttpRequest) {
|
|
65
|
+
// Client-side: context -> headers
|
|
66
|
+
const contextValue = contextKey.get();
|
|
67
|
+
if (contextValue !== undefined) {
|
|
68
|
+
const result = codec.decode(contextValue);
|
|
69
|
+
if (result.success) {
|
|
70
|
+
Object.assign(req.headers, result.value);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// Clear any default headers by decoding an empty context
|
|
74
|
+
const emptyResult = codec.decode({} as Input);
|
|
75
|
+
if (emptyResult.success) {
|
|
76
|
+
for (const key of Object.keys(emptyResult.value as object)) {
|
|
77
|
+
delete req.headers[key];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
parseRequest(req: http.IncomingMessage) {
|
|
83
|
+
// Server-side: headers -> context
|
|
84
|
+
const headers = req.headers as Record<string, string>;
|
|
85
|
+
const result = codec.encode(headers);
|
|
86
|
+
if (result.success) {
|
|
87
|
+
contextKey.set(result.value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this._middlewares.push(middleware);
|
|
93
|
+
return this as unknown as GGHttpSchemaBuilder<TContract, TContext | Input>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
routes(mapping: { [K in keyof TContract]: GGHttpCodec }): GGHttpSchema<TContract, TContext> {
|
|
97
|
+
return new GGHttpSchema(this._pathPrefix, this._contract, mapping, this._middlewares)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {GGHttpSchema} from "../schema/GGHttpSchema";
|
|
2
|
+
import {GGContractApiDefinition, GGContractImplementation} from "@grest-ts/schema";
|
|
3
|
+
import {GGHttpServerMiddleware} from "./GGHttpSchema.startServer";
|
|
4
|
+
import {GGHttpServer} from "./GGHttpServer";
|
|
5
|
+
|
|
6
|
+
export class GGHttp<TContext = undefined> {
|
|
7
|
+
|
|
8
|
+
private readonly httpServer: GGHttpServer
|
|
9
|
+
private readonly middlewares: GGHttpServerMiddleware[] = [];
|
|
10
|
+
|
|
11
|
+
constructor(httpServer: GGHttpServer) {
|
|
12
|
+
this.httpServer = httpServer;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public use<M extends GGHttpServerMiddleware>(middleware: M): GGHttp<TContext | M> {
|
|
16
|
+
this.middlewares.push(middleware);
|
|
17
|
+
return this as any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public http<TContract extends GGContractApiDefinition, TSchemaContext>(
|
|
21
|
+
schema: GGHttpSchema<TContract, TSchemaContext>,
|
|
22
|
+
implementation: GGContractImplementation<TContract>
|
|
23
|
+
): this {
|
|
24
|
+
schema.register(implementation, {http: this.httpServer, middlewares: this.middlewares});
|
|
25
|
+
return this as any;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {GGCounterKey, GGHistogramKey, GGMetrics} from "@grest-ts/metrics";
|
|
2
|
+
import {EXISTS, FORBIDDEN, NOT_AUTHORIZED, NOT_FOUND, OK, ROUTE_NOT_FOUND, SERVER_ERROR, VALIDATION_ERROR} from "@grest-ts/schema";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Result type for HTTP operations.
|
|
6
|
+
* Known types listed for documentation, but accepts any string for custom error types.
|
|
7
|
+
*/
|
|
8
|
+
type ResultType =
|
|
9
|
+
| typeof OK.TYPE
|
|
10
|
+
| typeof VALIDATION_ERROR.TYPE
|
|
11
|
+
| typeof NOT_AUTHORIZED.TYPE
|
|
12
|
+
| typeof FORBIDDEN.TYPE
|
|
13
|
+
| typeof NOT_FOUND.TYPE
|
|
14
|
+
| typeof ROUTE_NOT_FOUND.TYPE
|
|
15
|
+
| typeof EXISTS.TYPE
|
|
16
|
+
| typeof SERVER_ERROR.TYPE
|
|
17
|
+
| string;
|
|
18
|
+
|
|
19
|
+
export const GGHttpMetrics = GGMetrics.define('/http/', () => ({
|
|
20
|
+
requests: new GGCounterKey<{ api: string, method: string, path: string, result: ResultType }>('requests_total', {
|
|
21
|
+
help: 'Total HTTP requests after contract validation',
|
|
22
|
+
labelNames: ['api', 'method', 'path', 'result'],
|
|
23
|
+
groupBy: {labels: ["api", "method"], template: "{api}.{method}"}
|
|
24
|
+
}),
|
|
25
|
+
requestDuration: new GGHistogramKey<{ api: string, method: string, path: string }>('request_duration_ms', {
|
|
26
|
+
help: 'HTTP request duration in milliseconds',
|
|
27
|
+
labelNames: ['api', 'method', 'path'],
|
|
28
|
+
buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000],
|
|
29
|
+
groupBy: {labels: ["api", "method"], template: "{api}.{method}"}
|
|
30
|
+
}),
|
|
31
|
+
}));
|