@discover-cloud/shared 1.0.4 → 1.0.6

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.
@@ -3,29 +3,34 @@ import { Request } from "express";
3
3
  /**
4
4
  * SERVICE CLIENT
5
5
  * ───────────────
6
- * HTTP client for service-to-service calls.
7
- * Forwards the internal JWT and request ID from the current Express request.
6
+ * HTTP client for service-to-service communication.
8
7
  *
9
- * Fixes vs original:
10
- * - Removed `validateStatus: () => true` — it silently swallowed 4xx/5xx,
11
- * forcing every caller to manually check response.status. Removed so
12
- * axios throws naturally on error responses.
13
- * - `postWithAuth(url, data: any)` → `data: unknown` — no silent any
14
- * - requestId fallback now consistent with requestId middleware logic
15
- * - Added `patchWithAuth` and `deleteWithAuth` — common enough to include
16
- * - Retry only on network errors + 5xx (not 4xx those are caller errors)
8
+ * Two call patterns:
9
+ *
10
+ * 1. JWT-forwarding (user-initiated requests)
11
+ * Pass the current Express Request — Authorization + x-request-id
12
+ * are forwarded automatically.
13
+ * Use: getWithAuth, postWithAuth, patchWithAuth, deleteWithAuth
14
+ *
15
+ * 2. Internal calls (service-to-service, no incoming request)
16
+ * Pass explicit headers — used by UserServiceClient in auth service
17
+ * where calls originate from service logic, not from an HTTP handler.
18
+ * Use: get, post, patch, delete
19
+ *
20
+ * Retry strategy: network errors + 5xx only. Never retries 4xx —
21
+ * those are deterministic caller errors.
17
22
  */
18
23
  export declare class ServiceClient {
19
24
  readonly http: AxiosInstance;
20
25
  constructor(baseURL: string);
21
- private setupRetry;
22
26
  getWithAuth<T = unknown>(url: string, req: Request, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
23
27
  postWithAuth<T = unknown>(url: string, data: unknown, req: Request, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
24
28
  patchWithAuth<T = unknown>(url: string, data: unknown, req: Request, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
25
29
  deleteWithAuth<T = unknown>(url: string, req: Request, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
26
- /**
27
- * Merges Authorization + x-request-id into any provided config.
28
- * The internal JWT is forwarded as-is the gateway already minted it.
29
- */
30
+ get<T = unknown>(url: string, headers: Record<string, string>, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
31
+ post<T = unknown>(url: string, data: unknown, headers: Record<string, string>, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
32
+ patch<T = unknown>(url: string, data: unknown, headers: Record<string, string>, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
33
+ delete<T = unknown>(url: string, headers: Record<string, string>, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
30
34
  private mergeAuth;
35
+ private mergeHeaders;
31
36
  }
@@ -9,17 +9,22 @@ const axios_retry_1 = __importDefault(require("axios-retry"));
9
9
  /**
10
10
  * SERVICE CLIENT
11
11
  * ───────────────
12
- * HTTP client for service-to-service calls.
13
- * Forwards the internal JWT and request ID from the current Express request.
12
+ * HTTP client for service-to-service communication.
14
13
  *
15
- * Fixes vs original:
16
- * - Removed `validateStatus: () => true` — it silently swallowed 4xx/5xx,
17
- * forcing every caller to manually check response.status. Removed so
18
- * axios throws naturally on error responses.
19
- * - `postWithAuth(url, data: any)` → `data: unknown` — no silent any
20
- * - requestId fallback now consistent with requestId middleware logic
21
- * - Added `patchWithAuth` and `deleteWithAuth` — common enough to include
22
- * - Retry only on network errors + 5xx (not 4xx those are caller errors)
14
+ * Two call patterns:
15
+ *
16
+ * 1. JWT-forwarding (user-initiated requests)
17
+ * Pass the current Express Request — Authorization + x-request-id
18
+ * are forwarded automatically.
19
+ * Use: getWithAuth, postWithAuth, patchWithAuth, deleteWithAuth
20
+ *
21
+ * 2. Internal calls (service-to-service, no incoming request)
22
+ * Pass explicit headers — used by UserServiceClient in auth service
23
+ * where calls originate from service logic, not from an HTTP handler.
24
+ * Use: get, post, patch, delete
25
+ *
26
+ * Retry strategy: network errors + 5xx only. Never retries 4xx —
27
+ * those are deterministic caller errors.
23
28
  */
24
29
  class ServiceClient {
25
30
  constructor(baseURL) {
@@ -27,15 +32,10 @@ class ServiceClient {
27
32
  baseURL,
28
33
  timeout: 8000,
29
34
  });
30
- this.setupRetry();
31
- }
32
- setupRetry() {
33
35
  (0, axios_retry_1.default)(this.http, {
34
36
  retries: 3,
35
37
  retryDelay: axios_retry_1.default.exponentialDelay,
36
38
  retryCondition: (err) => {
37
- // Retry on network errors or 5xx only
38
- // Do NOT retry 4xx — those are deterministic failures
39
39
  if (axios_retry_1.default.isNetworkError(err))
40
40
  return true;
41
41
  const status = err.response?.status ?? 0;
@@ -44,10 +44,8 @@ class ServiceClient {
44
44
  });
45
45
  }
46
46
  /* ----------------------------------------------------------------
47
- Auth-forwarding methods
48
- Pass the current Express Request to automatically forward:
49
- - Authorization: Bearer <internal-jwt>
50
- - x-request-id: <propagated request ID>
47
+ Pattern 1 — JWT-forwarding (pass Express req)
48
+ Forwards Authorization header + x-request-id automatically.
51
49
  ---------------------------------------------------------------- */
52
50
  async getWithAuth(url, req, config) {
53
51
  return this.http.get(url, this.mergeAuth(req, config));
@@ -61,17 +59,28 @@ class ServiceClient {
61
59
  async deleteWithAuth(url, req, config) {
62
60
  return this.http.delete(url, this.mergeAuth(req, config));
63
61
  }
62
+ /* ----------------------------------------------------------------
63
+ Pattern 2 — Internal calls (explicit headers, no Express req)
64
+ Used for service-initiated calls (e.g. auth service → user service)
65
+ where there is no incoming HTTP request to forward from.
66
+ ---------------------------------------------------------------- */
67
+ async get(url, headers, config) {
68
+ return this.http.get(url, this.mergeHeaders(headers, config));
69
+ }
70
+ async post(url, data, headers, config) {
71
+ return this.http.post(url, data, this.mergeHeaders(headers, config));
72
+ }
73
+ async patch(url, data, headers, config) {
74
+ return this.http.patch(url, data, this.mergeHeaders(headers, config));
75
+ }
76
+ async delete(url, headers, config) {
77
+ return this.http.delete(url, this.mergeHeaders(headers, config));
78
+ }
64
79
  /* ----------------------------------------------------------------
65
80
  Private helpers
66
81
  ---------------------------------------------------------------- */
67
- /**
68
- * Merges Authorization + x-request-id into any provided config.
69
- * The internal JWT is forwarded as-is — the gateway already minted it.
70
- */
71
82
  mergeAuth(req, config) {
72
83
  const authHeader = req.headers.authorization;
73
- // req.id is set by requestId middleware — always a string at this point.
74
- // Fall back to x-request-id header if somehow req.id isn't set yet.
75
84
  const upstream = req.headers["x-request-id"];
76
85
  const requestId = req.id
77
86
  ?? (Array.isArray(upstream) ? upstream[0] : upstream)
@@ -85,5 +94,14 @@ class ServiceClient {
85
94
  },
86
95
  };
87
96
  }
97
+ mergeHeaders(headers, config) {
98
+ return {
99
+ ...config,
100
+ headers: {
101
+ ...config?.headers,
102
+ ...headers,
103
+ },
104
+ };
105
+ }
88
106
  }
89
107
  exports.ServiceClient = ServiceClient;
@@ -2,3 +2,4 @@ export * from "./error-handler.middleware";
2
2
  export * from "./validate.middleware";
3
3
  export * from "./request-id.middleware";
4
4
  export * from "./authorize.middleware";
5
+ export * from "./validated-merge.middleware";
@@ -18,3 +18,4 @@ __exportStar(require("./error-handler.middleware"), exports);
18
18
  __exportStar(require("./validate.middleware"), exports);
19
19
  __exportStar(require("./request-id.middleware"), exports);
20
20
  __exportStar(require("./authorize.middleware"), exports);
21
+ __exportStar(require("./validated-merge.middleware"), exports);
@@ -0,0 +1,20 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ /**
3
+ * VALIDATED MERGE HELPERS
4
+ * ─────────────────────────
5
+ * Used in routes that run two Validator.validate calls (e.g. params + body,
6
+ * or params + query). Each call overwrites req.validated, so values from
7
+ * the first validator must be captured before the second runs.
8
+ *
9
+ * Pattern:
10
+ * Validator.validate(paramsSchema, "params"), // req.validated = { id }
11
+ * captureValidated("id", "_capturedId"), // stashes id
12
+ * Validator.validate(bodySchema, "body"), // req.validated = { role }
13
+ * mergeValidated("id", "_capturedId"), // req.validated = { id, role }
14
+ *
15
+ * The cast through unknown is required — TypeScript won't allow a direct
16
+ * cast from Request to Record<string, unknown> because they don't overlap
17
+ * enough. Casting to unknown first tells TypeScript we know what we're doing.
18
+ */
19
+ export declare const captureValidated: (key: string, tempKey: string) => (req: Request, _res: Response, next: NextFunction) => void;
20
+ export declare const mergeValidated: (key: string, tempKey: string) => (req: Request, _res: Response, next: NextFunction) => void;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeValidated = exports.captureValidated = void 0;
4
+ /**
5
+ * VALIDATED MERGE HELPERS
6
+ * ─────────────────────────
7
+ * Used in routes that run two Validator.validate calls (e.g. params + body,
8
+ * or params + query). Each call overwrites req.validated, so values from
9
+ * the first validator must be captured before the second runs.
10
+ *
11
+ * Pattern:
12
+ * Validator.validate(paramsSchema, "params"), // req.validated = { id }
13
+ * captureValidated("id", "_capturedId"), // stashes id
14
+ * Validator.validate(bodySchema, "body"), // req.validated = { role }
15
+ * mergeValidated("id", "_capturedId"), // req.validated = { id, role }
16
+ *
17
+ * The cast through unknown is required — TypeScript won't allow a direct
18
+ * cast from Request to Record<string, unknown> because they don't overlap
19
+ * enough. Casting to unknown first tells TypeScript we know what we're doing.
20
+ */
21
+ const captureValidated = (key, tempKey) => (req, _res, next) => {
22
+ req[tempKey] =
23
+ req.validated[key];
24
+ next();
25
+ };
26
+ exports.captureValidated = captureValidated;
27
+ const mergeValidated = (key, tempKey) => (req, _res, next) => {
28
+ req.validated = {
29
+ ...req.validated,
30
+ [key]: req[tempKey],
31
+ };
32
+ next();
33
+ };
34
+ exports.mergeValidated = mergeValidated;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discover-cloud/shared",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",