@hazeljs/gateway 0.2.0-beta.41
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 +192 -0
- package/README.md +255 -0
- package/dist/__tests__/canary-engine.test.d.ts +2 -0
- package/dist/__tests__/canary-engine.test.d.ts.map +1 -0
- package/dist/__tests__/canary-engine.test.js +133 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +174 -0
- package/dist/__tests__/from-config.test.d.ts +2 -0
- package/dist/__tests__/from-config.test.d.ts.map +1 -0
- package/dist/__tests__/from-config.test.js +67 -0
- package/dist/__tests__/gateway-metrics.test.d.ts +2 -0
- package/dist/__tests__/gateway-metrics.test.d.ts.map +1 -0
- package/dist/__tests__/gateway-metrics.test.js +82 -0
- package/dist/__tests__/gateway-module.test.d.ts +2 -0
- package/dist/__tests__/gateway-module.test.d.ts.map +1 -0
- package/dist/__tests__/gateway-module.test.js +91 -0
- package/dist/__tests__/gateway.test.d.ts +2 -0
- package/dist/__tests__/gateway.test.d.ts.map +1 -0
- package/dist/__tests__/gateway.test.js +257 -0
- package/dist/__tests__/hazel-integration.test.d.ts +2 -0
- package/dist/__tests__/hazel-integration.test.d.ts.map +1 -0
- package/dist/__tests__/hazel-integration.test.js +92 -0
- package/dist/__tests__/route-matcher.test.d.ts +2 -0
- package/dist/__tests__/route-matcher.test.d.ts.map +1 -0
- package/dist/__tests__/route-matcher.test.js +67 -0
- package/dist/__tests__/service-proxy.test.d.ts +2 -0
- package/dist/__tests__/service-proxy.test.d.ts.map +1 -0
- package/dist/__tests__/service-proxy.test.js +110 -0
- package/dist/__tests__/traffic-mirror.test.d.ts +2 -0
- package/dist/__tests__/traffic-mirror.test.d.ts.map +1 -0
- package/dist/__tests__/traffic-mirror.test.js +70 -0
- package/dist/__tests__/version-router.test.d.ts +2 -0
- package/dist/__tests__/version-router.test.d.ts.map +1 -0
- package/dist/__tests__/version-router.test.js +136 -0
- package/dist/canary/canary-engine.d.ts +107 -0
- package/dist/canary/canary-engine.d.ts.map +1 -0
- package/dist/canary/canary-engine.js +334 -0
- package/dist/decorators/index.d.ts +74 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +170 -0
- package/dist/gateway.d.ts +67 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +310 -0
- package/dist/gateway.module.d.ts +67 -0
- package/dist/gateway.module.d.ts.map +1 -0
- package/dist/gateway.module.js +82 -0
- package/dist/hazel-integration.d.ts +24 -0
- package/dist/hazel-integration.d.ts.map +1 -0
- package/dist/hazel-integration.js +70 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/metrics/gateway-metrics.d.ts +64 -0
- package/dist/metrics/gateway-metrics.d.ts.map +1 -0
- package/dist/metrics/gateway-metrics.js +159 -0
- package/dist/middleware/traffic-mirror.d.ts +19 -0
- package/dist/middleware/traffic-mirror.d.ts.map +1 -0
- package/dist/middleware/traffic-mirror.js +60 -0
- package/dist/proxy/service-proxy.d.ts +68 -0
- package/dist/proxy/service-proxy.d.ts.map +1 -0
- package/dist/proxy/service-proxy.js +211 -0
- package/dist/routing/route-matcher.d.ts +31 -0
- package/dist/routing/route-matcher.d.ts.map +1 -0
- package/dist/routing/route-matcher.js +112 -0
- package/dist/routing/version-router.d.ts +36 -0
- package/dist/routing/version-router.d.ts.map +1 -0
- package/dist/routing/version-router.js +136 -0
- package/dist/types/index.d.ts +217 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +17 -0
- package/package.json +74 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Gateway Decorators
|
|
4
|
+
* Declarative API for defining gateway routes and policies.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Gateway = Gateway;
|
|
8
|
+
exports.Route = Route;
|
|
9
|
+
exports.ServiceRoute = ServiceRoute;
|
|
10
|
+
exports.VersionRoute = VersionRoute;
|
|
11
|
+
exports.Canary = Canary;
|
|
12
|
+
exports.TrafficPolicy = TrafficPolicy;
|
|
13
|
+
exports.GatewayCircuitBreaker = GatewayCircuitBreaker;
|
|
14
|
+
exports.GatewayRateLimit = GatewayRateLimit;
|
|
15
|
+
exports.getGatewayConfig = getGatewayConfig;
|
|
16
|
+
exports.getRouteConfig = getRouteConfig;
|
|
17
|
+
exports.getServiceRouteConfig = getServiceRouteConfig;
|
|
18
|
+
exports.getVersionRouteConfig = getVersionRouteConfig;
|
|
19
|
+
exports.getCanaryConfig = getCanaryConfig;
|
|
20
|
+
exports.getTrafficPolicyConfig = getTrafficPolicyConfig;
|
|
21
|
+
exports.getCircuitBreakerConfig = getCircuitBreakerConfig;
|
|
22
|
+
exports.getRateLimitConfig = getRateLimitConfig;
|
|
23
|
+
exports.collectRouteDefinitions = collectRouteDefinitions;
|
|
24
|
+
require("reflect-metadata");
|
|
25
|
+
// Metadata keys
|
|
26
|
+
const GATEWAY_CONFIG_KEY = Symbol('gateway:config');
|
|
27
|
+
const GATEWAY_PROPERTIES_KEY = Symbol('gateway:properties');
|
|
28
|
+
const ROUTE_KEY = Symbol('gateway:route');
|
|
29
|
+
const SERVICE_ROUTE_KEY = Symbol('gateway:serviceRoute');
|
|
30
|
+
const VERSION_ROUTE_KEY = Symbol('gateway:versionRoute');
|
|
31
|
+
const CANARY_KEY = Symbol('gateway:canary');
|
|
32
|
+
const TRAFFIC_POLICY_KEY = Symbol('gateway:trafficPolicy');
|
|
33
|
+
const CIRCUIT_BREAKER_KEY = Symbol('gateway:circuitBreaker');
|
|
34
|
+
const RATE_LIMIT_KEY = Symbol('gateway:rateLimit');
|
|
35
|
+
/**
|
|
36
|
+
* Track a property as a gateway-decorated property.
|
|
37
|
+
* This is needed because TypeScript class property declarations don't appear
|
|
38
|
+
* on the prototype, so we can't discover them via Object.getOwnPropertyNames.
|
|
39
|
+
*/
|
|
40
|
+
function trackProperty(target, propertyKey) {
|
|
41
|
+
const existing = Reflect.getMetadata(GATEWAY_PROPERTIES_KEY, target) || new Set();
|
|
42
|
+
existing.add(String(propertyKey));
|
|
43
|
+
Reflect.defineMetadata(GATEWAY_PROPERTIES_KEY, existing, target);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @Gateway class decorator
|
|
47
|
+
* Marks a class as a gateway definition with global configuration.
|
|
48
|
+
*/
|
|
49
|
+
function Gateway(config) {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- ClassDecorator requires Function
|
|
51
|
+
return function (target) {
|
|
52
|
+
Reflect.defineMetadata(GATEWAY_CONFIG_KEY, config, target);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* @Route property/method decorator
|
|
57
|
+
* Defines a URL pattern this route handles.
|
|
58
|
+
*/
|
|
59
|
+
function Route(pathOrConfig) {
|
|
60
|
+
return function (target, propertyKey) {
|
|
61
|
+
const config = typeof pathOrConfig === 'string' ? { path: pathOrConfig } : pathOrConfig;
|
|
62
|
+
Reflect.defineMetadata(ROUTE_KEY, config, target, propertyKey);
|
|
63
|
+
trackProperty(target, propertyKey);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* @ServiceRoute property/method decorator
|
|
68
|
+
* Maps this route to a service in the discovery registry.
|
|
69
|
+
*/
|
|
70
|
+
function ServiceRoute(nameOrConfig) {
|
|
71
|
+
return function (target, propertyKey) {
|
|
72
|
+
const config = typeof nameOrConfig === 'string' ? { serviceName: nameOrConfig } : nameOrConfig;
|
|
73
|
+
Reflect.defineMetadata(SERVICE_ROUTE_KEY, config, target, propertyKey);
|
|
74
|
+
trackProperty(target, propertyKey);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* @VersionRoute property/method decorator
|
|
79
|
+
* Enables version-based routing for this route.
|
|
80
|
+
*/
|
|
81
|
+
function VersionRoute(config) {
|
|
82
|
+
return function (target, propertyKey) {
|
|
83
|
+
Reflect.defineMetadata(VERSION_ROUTE_KEY, config, target, propertyKey);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* @Canary property/method decorator
|
|
88
|
+
* Enables canary deployment for this route.
|
|
89
|
+
*/
|
|
90
|
+
function Canary(config) {
|
|
91
|
+
return function (target, propertyKey) {
|
|
92
|
+
Reflect.defineMetadata(CANARY_KEY, config, target, propertyKey);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* @TrafficPolicy property/method decorator
|
|
97
|
+
* Defines traffic policies (mirroring, transformation, etc.)
|
|
98
|
+
*/
|
|
99
|
+
function TrafficPolicy(config) {
|
|
100
|
+
return function (target, propertyKey) {
|
|
101
|
+
Reflect.defineMetadata(TRAFFIC_POLICY_KEY, config, target, propertyKey);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* @CircuitBreaker property decorator (gateway-specific)
|
|
106
|
+
* Overrides circuit breaker configuration for this route.
|
|
107
|
+
*/
|
|
108
|
+
function GatewayCircuitBreaker(config) {
|
|
109
|
+
return function (target, propertyKey) {
|
|
110
|
+
Reflect.defineMetadata(CIRCUIT_BREAKER_KEY, config, target, propertyKey);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* @RateLimit property decorator (gateway-specific)
|
|
115
|
+
* Overrides rate limit configuration for this route.
|
|
116
|
+
*/
|
|
117
|
+
function GatewayRateLimit(config) {
|
|
118
|
+
return function (target, propertyKey) {
|
|
119
|
+
Reflect.defineMetadata(RATE_LIMIT_KEY, config, target, propertyKey);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// ─── Metadata Readers ───
|
|
123
|
+
function getGatewayConfig(target) {
|
|
124
|
+
return Reflect.getMetadata(GATEWAY_CONFIG_KEY, target);
|
|
125
|
+
}
|
|
126
|
+
function getRouteConfig(target, propertyKey) {
|
|
127
|
+
return Reflect.getMetadata(ROUTE_KEY, target, propertyKey);
|
|
128
|
+
}
|
|
129
|
+
function getServiceRouteConfig(target, propertyKey) {
|
|
130
|
+
return Reflect.getMetadata(SERVICE_ROUTE_KEY, target, propertyKey);
|
|
131
|
+
}
|
|
132
|
+
function getVersionRouteConfig(target, propertyKey) {
|
|
133
|
+
return Reflect.getMetadata(VERSION_ROUTE_KEY, target, propertyKey);
|
|
134
|
+
}
|
|
135
|
+
function getCanaryConfig(target, propertyKey) {
|
|
136
|
+
return Reflect.getMetadata(CANARY_KEY, target, propertyKey);
|
|
137
|
+
}
|
|
138
|
+
function getTrafficPolicyConfig(target, propertyKey) {
|
|
139
|
+
return Reflect.getMetadata(TRAFFIC_POLICY_KEY, target, propertyKey);
|
|
140
|
+
}
|
|
141
|
+
function getCircuitBreakerConfig(target, propertyKey) {
|
|
142
|
+
return Reflect.getMetadata(CIRCUIT_BREAKER_KEY, target, propertyKey);
|
|
143
|
+
}
|
|
144
|
+
function getRateLimitConfig(target, propertyKey) {
|
|
145
|
+
return Reflect.getMetadata(RATE_LIMIT_KEY, target, propertyKey);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Collect all route definitions from a gateway class instance
|
|
149
|
+
*/
|
|
150
|
+
function collectRouteDefinitions(gatewayClass) {
|
|
151
|
+
const config = getGatewayConfig(gatewayClass) || {};
|
|
152
|
+
const prototype = gatewayClass.prototype;
|
|
153
|
+
// Get tracked gateway properties (from decorators) + prototype methods
|
|
154
|
+
const trackedProps = Reflect.getMetadata(GATEWAY_PROPERTIES_KEY, prototype) || new Set();
|
|
155
|
+
const protoMethods = Object.getOwnPropertyNames(prototype).filter((p) => p !== 'constructor');
|
|
156
|
+
const allProperties = new Set([...trackedProps, ...protoMethods]);
|
|
157
|
+
const routes = Array.from(allProperties)
|
|
158
|
+
.map((propertyKey) => ({
|
|
159
|
+
propertyKey,
|
|
160
|
+
route: getRouteConfig(prototype, propertyKey),
|
|
161
|
+
serviceRoute: getServiceRouteConfig(prototype, propertyKey),
|
|
162
|
+
versionRoute: getVersionRouteConfig(prototype, propertyKey),
|
|
163
|
+
canary: getCanaryConfig(prototype, propertyKey),
|
|
164
|
+
trafficPolicy: getTrafficPolicyConfig(prototype, propertyKey),
|
|
165
|
+
circuitBreaker: getCircuitBreakerConfig(prototype, propertyKey),
|
|
166
|
+
rateLimit: getRateLimitConfig(prototype, propertyKey),
|
|
167
|
+
}))
|
|
168
|
+
.filter((r) => r.route || r.serviceRoute);
|
|
169
|
+
return { config, routes };
|
|
170
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway
|
|
3
|
+
* The main orchestrator that ties together routing, versioning, canary,
|
|
4
|
+
* resilience, and traffic policies into a unified request handling pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Can be used declaratively via decorators or programmatically via config.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import { DiscoveryClient, RegistryBackend } from '@hazeljs/discovery';
|
|
10
|
+
import { GatewayConfig, GatewayFullConfig, GatewayRouteDefinition, ProxyRequest, ProxyResponse } from './types';
|
|
11
|
+
import { CanaryEngine } from './canary/canary-engine';
|
|
12
|
+
import { GatewayMetrics } from './metrics/gateway-metrics';
|
|
13
|
+
export declare class GatewayServer extends EventEmitter {
|
|
14
|
+
private discoveryClient;
|
|
15
|
+
private routes;
|
|
16
|
+
private sortedPatterns;
|
|
17
|
+
private metrics;
|
|
18
|
+
private config;
|
|
19
|
+
constructor(config: GatewayConfig, backend?: RegistryBackend);
|
|
20
|
+
/**
|
|
21
|
+
* Create a gateway from a plain configuration object.
|
|
22
|
+
* This is the production-recommended approach — config values come from
|
|
23
|
+
* env vars / config files via @hazeljs/config instead of hardcoded decorators.
|
|
24
|
+
*/
|
|
25
|
+
static fromConfig(config: GatewayFullConfig, backend?: RegistryBackend): GatewayServer;
|
|
26
|
+
/**
|
|
27
|
+
* Create a gateway from a decorated class
|
|
28
|
+
*/
|
|
29
|
+
static fromClass(gatewayClass: new (...args: unknown[]) => unknown, backend?: RegistryBackend): GatewayServer;
|
|
30
|
+
/**
|
|
31
|
+
* Add a route definition to the gateway
|
|
32
|
+
*/
|
|
33
|
+
addRoute(definition: GatewayRouteDefinition): void;
|
|
34
|
+
/**
|
|
35
|
+
* Handle an incoming request through the gateway pipeline
|
|
36
|
+
*/
|
|
37
|
+
handleRequest(request: ProxyRequest): Promise<ProxyResponse>;
|
|
38
|
+
/**
|
|
39
|
+
* Start all canary engines
|
|
40
|
+
*/
|
|
41
|
+
startCanaries(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Stop all canary engines and clean up
|
|
44
|
+
*/
|
|
45
|
+
stop(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Get the gateway metrics
|
|
48
|
+
*/
|
|
49
|
+
getMetrics(): GatewayMetrics;
|
|
50
|
+
/**
|
|
51
|
+
* Get the canary engine for a specific route
|
|
52
|
+
*/
|
|
53
|
+
getCanaryEngine(routePath: string): CanaryEngine | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Get all route paths
|
|
56
|
+
*/
|
|
57
|
+
getRoutes(): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Get the discovery client
|
|
60
|
+
*/
|
|
61
|
+
getDiscoveryClient(): DiscoveryClient;
|
|
62
|
+
private findHandler;
|
|
63
|
+
private handleCanaryRequest;
|
|
64
|
+
private handleVersionedRequest;
|
|
65
|
+
private createCanaryEngine;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,sBAAsB,EACtB,YAAY,EACZ,aAAa,EAGd,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAa3D,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,MAAM,CAAgB;gBAElB,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,eAAe;IAQ5D;;;;OAIG;IACH,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,aAAa;IAQtF;;OAEG;IACH,MAAM,CAAC,SAAS,CACd,YAAY,EAAE,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EACjD,OAAO,CAAC,EAAE,eAAe,GACxB,aAAa;IAuBhB;;OAEG;IACH,QAAQ,CAAC,UAAU,EAAE,sBAAsB,GAAG,IAAI;IA8ClD;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IA6FlE;;OAEG;IACH,aAAa,IAAI,IAAI;IAQrB;;OAEG;IACH,IAAI,IAAI,IAAI;IASZ;;OAEG;IACH,UAAU,IAAI,cAAc;IAI5B;;OAEG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI5D;;OAEG;IACH,SAAS,IAAI,MAAM,EAAE;IAIrB;;OAEG;IACH,kBAAkB,IAAI,eAAe;IAMrC,OAAO,CAAC,WAAW;YAUL,mBAAmB;YAoCnB,sBAAsB;IAgCpC,OAAO,CAAC,kBAAkB;CA2B3B"}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Gateway
|
|
4
|
+
* The main orchestrator that ties together routing, versioning, canary,
|
|
5
|
+
* resilience, and traffic policies into a unified request handling pipeline.
|
|
6
|
+
*
|
|
7
|
+
* Can be used declaratively via decorators or programmatically via config.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.GatewayServer = void 0;
|
|
11
|
+
const events_1 = require("events");
|
|
12
|
+
const discovery_1 = require("@hazeljs/discovery");
|
|
13
|
+
const resilience_1 = require("@hazeljs/resilience");
|
|
14
|
+
const service_proxy_1 = require("./proxy/service-proxy");
|
|
15
|
+
const version_router_1 = require("./routing/version-router");
|
|
16
|
+
const canary_engine_1 = require("./canary/canary-engine");
|
|
17
|
+
const gateway_metrics_1 = require("./metrics/gateway-metrics");
|
|
18
|
+
const traffic_mirror_1 = require("./middleware/traffic-mirror");
|
|
19
|
+
const route_matcher_1 = require("./routing/route-matcher");
|
|
20
|
+
const decorators_1 = require("./decorators");
|
|
21
|
+
class GatewayServer extends events_1.EventEmitter {
|
|
22
|
+
constructor(config, backend) {
|
|
23
|
+
super();
|
|
24
|
+
this.routes = new Map();
|
|
25
|
+
this.sortedPatterns = [];
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.metrics = new gateway_metrics_1.GatewayMetrics(config.metrics?.windowSize ?? 60000);
|
|
28
|
+
this.discoveryClient = new discovery_1.DiscoveryClient(config.discovery ?? {}, backend);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a gateway from a plain configuration object.
|
|
32
|
+
* This is the production-recommended approach — config values come from
|
|
33
|
+
* env vars / config files via @hazeljs/config instead of hardcoded decorators.
|
|
34
|
+
*/
|
|
35
|
+
static fromConfig(config, backend) {
|
|
36
|
+
const gateway = new GatewayServer(config, backend);
|
|
37
|
+
for (const route of config.routes) {
|
|
38
|
+
gateway.addRoute(route);
|
|
39
|
+
}
|
|
40
|
+
return gateway;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a gateway from a decorated class
|
|
44
|
+
*/
|
|
45
|
+
static fromClass(gatewayClass, backend) {
|
|
46
|
+
const { config, routes } = (0, decorators_1.collectRouteDefinitions)(gatewayClass);
|
|
47
|
+
const gateway = new GatewayServer(config, backend);
|
|
48
|
+
for (const routeDef of routes) {
|
|
49
|
+
if (!routeDef.route || !routeDef.serviceRoute)
|
|
50
|
+
continue;
|
|
51
|
+
gateway.addRoute({
|
|
52
|
+
path: routeDef.route.path,
|
|
53
|
+
serviceName: routeDef.serviceRoute.serviceName,
|
|
54
|
+
serviceConfig: routeDef.serviceRoute,
|
|
55
|
+
versionRoute: routeDef.versionRoute,
|
|
56
|
+
canary: routeDef.canary,
|
|
57
|
+
trafficPolicy: routeDef.trafficPolicy,
|
|
58
|
+
circuitBreaker: routeDef.circuitBreaker,
|
|
59
|
+
rateLimit: routeDef.rateLimit,
|
|
60
|
+
methods: routeDef.route.methods,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return gateway;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Add a route definition to the gateway
|
|
67
|
+
*/
|
|
68
|
+
addRoute(definition) {
|
|
69
|
+
const proxyConfig = {
|
|
70
|
+
serviceName: definition.serviceName,
|
|
71
|
+
loadBalancingStrategy: definition.serviceConfig?.loadBalancingStrategy,
|
|
72
|
+
filter: definition.serviceConfig?.filter,
|
|
73
|
+
stripPrefix: definition.serviceConfig?.stripPrefix,
|
|
74
|
+
addPrefix: definition.serviceConfig?.addPrefix,
|
|
75
|
+
timeout: definition.trafficPolicy?.timeout ?? this.config.resilience?.defaultTimeout,
|
|
76
|
+
retry: definition.trafficPolicy?.retry ?? this.config.resilience?.defaultRetry,
|
|
77
|
+
circuitBreaker: definition.circuitBreaker ??
|
|
78
|
+
definition.trafficPolicy?.circuitBreaker ??
|
|
79
|
+
this.config.resilience?.defaultCircuitBreaker,
|
|
80
|
+
rateLimit: definition.rateLimit ?? definition.trafficPolicy?.rateLimit,
|
|
81
|
+
transform: definition.trafficPolicy?.transform,
|
|
82
|
+
};
|
|
83
|
+
const proxy = new service_proxy_1.ServiceProxy(this.discoveryClient, proxyConfig);
|
|
84
|
+
const handler = {
|
|
85
|
+
definition,
|
|
86
|
+
proxy,
|
|
87
|
+
};
|
|
88
|
+
// Set up version routing
|
|
89
|
+
if (definition.versionRoute) {
|
|
90
|
+
handler.versionRouter = new version_router_1.VersionRouter(definition.versionRoute);
|
|
91
|
+
}
|
|
92
|
+
// Set up canary deployment
|
|
93
|
+
if (definition.canary) {
|
|
94
|
+
handler.canaryEngine = this.createCanaryEngine(definition.canary, definition.path);
|
|
95
|
+
}
|
|
96
|
+
// Set up traffic mirroring
|
|
97
|
+
if (definition.trafficPolicy?.mirror) {
|
|
98
|
+
handler.trafficMirror = new traffic_mirror_1.TrafficMirror(definition.trafficPolicy.mirror, this.discoveryClient);
|
|
99
|
+
}
|
|
100
|
+
this.routes.set(definition.path, handler);
|
|
101
|
+
this.sortedPatterns = (0, route_matcher_1.sortRoutesBySpecificity)(Array.from(this.routes.keys()));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Handle an incoming request through the gateway pipeline
|
|
105
|
+
*/
|
|
106
|
+
async handleRequest(request) {
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
// 1. Match the route
|
|
109
|
+
const handler = this.findHandler(request);
|
|
110
|
+
if (!handler) {
|
|
111
|
+
return {
|
|
112
|
+
status: 404,
|
|
113
|
+
headers: {},
|
|
114
|
+
body: { error: 'No matching gateway route', path: request.path },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Check method
|
|
118
|
+
if (handler.definition.methods &&
|
|
119
|
+
handler.definition.methods.length > 0 &&
|
|
120
|
+
!handler.definition.methods.includes(request.method.toUpperCase())) {
|
|
121
|
+
return {
|
|
122
|
+
status: 405,
|
|
123
|
+
headers: {},
|
|
124
|
+
body: { error: 'Method not allowed' },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
let response;
|
|
129
|
+
// 2. Canary routing takes priority
|
|
130
|
+
if (handler.canaryEngine) {
|
|
131
|
+
response = await this.handleCanaryRequest(handler, request);
|
|
132
|
+
}
|
|
133
|
+
// 3. Version routing
|
|
134
|
+
else if (handler.versionRouter) {
|
|
135
|
+
response = await this.handleVersionedRequest(handler, request);
|
|
136
|
+
}
|
|
137
|
+
// 4. Direct proxy
|
|
138
|
+
else {
|
|
139
|
+
response = await handler.proxy.forward(request);
|
|
140
|
+
}
|
|
141
|
+
// 5. Mirror traffic (fire and forget)
|
|
142
|
+
if (handler.trafficMirror) {
|
|
143
|
+
handler.trafficMirror.mirror(request);
|
|
144
|
+
}
|
|
145
|
+
// 6. Record metrics
|
|
146
|
+
const duration = Date.now() - startTime;
|
|
147
|
+
const isError = response.status >= 500;
|
|
148
|
+
if (isError) {
|
|
149
|
+
this.metrics.recordFailure(handler.definition.path, duration, `HTTP ${response.status}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
this.metrics.recordSuccess(handler.definition.path, duration);
|
|
153
|
+
}
|
|
154
|
+
return response;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const duration = Date.now() - startTime;
|
|
158
|
+
this.metrics.recordFailure(handler.definition.path, duration, String(error));
|
|
159
|
+
this.emit('route:error', {
|
|
160
|
+
route: handler.definition.path,
|
|
161
|
+
service: handler.definition.serviceName,
|
|
162
|
+
error: String(error),
|
|
163
|
+
});
|
|
164
|
+
// Return 429 for rate limit exceeded (RFC 6585)
|
|
165
|
+
if (error instanceof resilience_1.RateLimitError) {
|
|
166
|
+
const retryAfterSec = error.retryAfterMs ? Math.ceil(error.retryAfterMs / 1000) : 60;
|
|
167
|
+
return {
|
|
168
|
+
status: 429,
|
|
169
|
+
headers: { 'Retry-After': String(retryAfterSec) },
|
|
170
|
+
body: {
|
|
171
|
+
error: 'Too Many Requests',
|
|
172
|
+
message: error.message,
|
|
173
|
+
retryAfter: retryAfterSec,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
status: 502,
|
|
179
|
+
headers: {},
|
|
180
|
+
body: {
|
|
181
|
+
error: 'Bad Gateway',
|
|
182
|
+
message: error instanceof Error ? error.message : String(error),
|
|
183
|
+
service: handler.definition.serviceName,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Start all canary engines
|
|
190
|
+
*/
|
|
191
|
+
startCanaries() {
|
|
192
|
+
for (const handler of this.routes.values()) {
|
|
193
|
+
if (handler.canaryEngine) {
|
|
194
|
+
handler.canaryEngine.start();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Stop all canary engines and clean up
|
|
200
|
+
*/
|
|
201
|
+
stop() {
|
|
202
|
+
for (const handler of this.routes.values()) {
|
|
203
|
+
if (handler.canaryEngine) {
|
|
204
|
+
handler.canaryEngine.stop();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
this.discoveryClient.close();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get the gateway metrics
|
|
211
|
+
*/
|
|
212
|
+
getMetrics() {
|
|
213
|
+
return this.metrics;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get the canary engine for a specific route
|
|
217
|
+
*/
|
|
218
|
+
getCanaryEngine(routePath) {
|
|
219
|
+
return this.routes.get(routePath)?.canaryEngine;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get all route paths
|
|
223
|
+
*/
|
|
224
|
+
getRoutes() {
|
|
225
|
+
return Array.from(this.routes.keys());
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get the discovery client
|
|
229
|
+
*/
|
|
230
|
+
getDiscoveryClient() {
|
|
231
|
+
return this.discoveryClient;
|
|
232
|
+
}
|
|
233
|
+
// ─── Internal ───
|
|
234
|
+
findHandler(request) {
|
|
235
|
+
for (const pattern of this.sortedPatterns) {
|
|
236
|
+
const match = (0, route_matcher_1.matchRoute)(pattern, request.path);
|
|
237
|
+
if (match.matched) {
|
|
238
|
+
return this.routes.get(pattern);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
async handleCanaryRequest(handler, request) {
|
|
244
|
+
const engine = handler.canaryEngine;
|
|
245
|
+
const target = engine.selectVersion(request);
|
|
246
|
+
const version = engine.getVersion(target);
|
|
247
|
+
const startTime = Date.now();
|
|
248
|
+
try {
|
|
249
|
+
const response = await handler.proxy.forwardToVersion(request, version);
|
|
250
|
+
const duration = Date.now() - startTime;
|
|
251
|
+
const isError = response.status >= 500;
|
|
252
|
+
if (isError) {
|
|
253
|
+
engine.recordFailure(target, duration, `HTTP ${response.status}`);
|
|
254
|
+
this.metrics.recordFailure(handler.definition.path, duration, `HTTP ${response.status}`, version);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
engine.recordSuccess(target, duration);
|
|
258
|
+
this.metrics.recordSuccess(handler.definition.path, duration, version);
|
|
259
|
+
}
|
|
260
|
+
return response;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const duration = Date.now() - startTime;
|
|
264
|
+
engine.recordFailure(target, duration, String(error));
|
|
265
|
+
this.metrics.recordFailure(handler.definition.path, duration, String(error), version);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async handleVersionedRequest(handler, request) {
|
|
270
|
+
const router = handler.versionRouter;
|
|
271
|
+
const resolution = router.resolve(request);
|
|
272
|
+
const startTime = Date.now();
|
|
273
|
+
try {
|
|
274
|
+
const entry = router.getVersionEntry(resolution.version);
|
|
275
|
+
const response = await handler.proxy.forwardToVersion(request, resolution.version, entry?.filter);
|
|
276
|
+
const duration = Date.now() - startTime;
|
|
277
|
+
this.metrics.recordSuccess(handler.definition.path, duration, resolution.version);
|
|
278
|
+
return response;
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
const duration = Date.now() - startTime;
|
|
282
|
+
this.metrics.recordFailure(handler.definition.path, duration, String(error), resolution.version);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
createCanaryEngine(config, routePath) {
|
|
287
|
+
const engine = new canary_engine_1.CanaryEngine(config);
|
|
288
|
+
// Forward canary events to the gateway
|
|
289
|
+
engine.on('canary:promote', (data) => {
|
|
290
|
+
this.emit('canary:promote', {
|
|
291
|
+
route: routePath,
|
|
292
|
+
...data,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
engine.on('canary:rollback', (data) => {
|
|
296
|
+
this.emit('canary:rollback', {
|
|
297
|
+
route: routePath,
|
|
298
|
+
...data,
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
engine.on('canary:complete', (data) => {
|
|
302
|
+
this.emit('canary:complete', {
|
|
303
|
+
route: routePath,
|
|
304
|
+
...data,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
return engine;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
exports.GatewayServer = GatewayServer;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Module
|
|
3
|
+
* Integrates @hazeljs/gateway with @hazeljs/config for config-driven routing.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* 1. Define a gateway config loader (gateway.config.ts) that reads env vars
|
|
7
|
+
* 2. Register it with ConfigModule.forRoot({ load: [gatewayConfig] })
|
|
8
|
+
* 3. Use GatewayModule.forRoot() to create the gateway from config
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* GatewayModule.forRoot({ configKey: 'gateway' })
|
|
12
|
+
* // reads ConfigService.get('gateway') -> GatewayFullConfig
|
|
13
|
+
*/
|
|
14
|
+
import { GatewayFullConfig } from './types';
|
|
15
|
+
export interface GatewayModuleOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Key in ConfigService to read gateway config from.
|
|
18
|
+
* The value at this key should conform to GatewayFullConfig.
|
|
19
|
+
* Default: 'gateway'
|
|
20
|
+
*/
|
|
21
|
+
configKey?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Provide gateway config directly (for programmatic use
|
|
24
|
+
* without @hazeljs/config dependency).
|
|
25
|
+
* If both `config` and `configKey` are set, `config` takes precedence.
|
|
26
|
+
*/
|
|
27
|
+
config?: GatewayFullConfig;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* GatewayModule follows the same forRoot() pattern as ConfigModule.
|
|
31
|
+
*
|
|
32
|
+
* It stores the options statically so the gateway can be created later
|
|
33
|
+
* when the app boots and ConfigService is available.
|
|
34
|
+
*/
|
|
35
|
+
export declare class GatewayModule {
|
|
36
|
+
private static options;
|
|
37
|
+
/**
|
|
38
|
+
* Register the gateway module with configuration options.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Config-driven (reads from ConfigService):
|
|
42
|
+
* GatewayModule.forRoot({ configKey: 'gateway' })
|
|
43
|
+
*
|
|
44
|
+
* // Direct config (no ConfigService needed):
|
|
45
|
+
* GatewayModule.forRoot({ config: { discovery: {...}, routes: [...] } })
|
|
46
|
+
*/
|
|
47
|
+
static forRoot(options?: GatewayModuleOptions): {
|
|
48
|
+
module: typeof GatewayModule;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Get the registered options.
|
|
52
|
+
*/
|
|
53
|
+
static getOptions(): GatewayModuleOptions;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the gateway configuration.
|
|
56
|
+
*
|
|
57
|
+
* If `config` was provided directly, returns it.
|
|
58
|
+
* Otherwise, reads from the provided ConfigService using the configKey.
|
|
59
|
+
*
|
|
60
|
+
* @param configService - Optional ConfigService instance. Required if
|
|
61
|
+
* no direct `config` was provided in forRoot().
|
|
62
|
+
*/
|
|
63
|
+
static resolveConfig(configService?: {
|
|
64
|
+
get: <T>(key: string) => T | undefined;
|
|
65
|
+
}): GatewayFullConfig;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=gateway.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.module.d.ts","sourceRoot":"","sources":["../src/gateway.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,OAAO,CAA4B;IAElD;;;;;;;;;OASG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG;QAAE,MAAM,EAAE,OAAO,aAAa,CAAA;KAAE;IAQpF;;OAEG;IACH,MAAM,CAAC,UAAU,IAAI,oBAAoB;IAIzC;;;;;;;;OAQG;IACH,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE;QACnC,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC,GAAG,SAAS,CAAC;KACxC,GAAG,iBAAiB;CAoCtB"}
|