ts-typed-api 0.2.14 → 0.2.16

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/dist/client.d.ts CHANGED
@@ -128,6 +128,18 @@ export declare class ApiClient<TActualDef extends BaseApiDefinitionSchema> {
128
128
  * @returns The base URL with prefix applied.
129
129
  */
130
130
  private getBaseUrlWithPrefix;
131
+ /**
132
+ * Generates the full URL for a specific route, incorporating path parameters and query parameters.
133
+ * @template TDomain The domain (controller) of the API.
134
+ * @template TRouteKey The key of the route within the domain.
135
+ * @param domain The API domain (e.g., 'user').
136
+ * @param routeKey The API route key (e.g., 'getUsers').
137
+ * @param params Optional path parameters to replace in the route path.
138
+ * @param query Optional query parameters to append to the URL.
139
+ * @returns The full URL as a string.
140
+ * @throws Error if the route configuration is invalid.
141
+ */
142
+ generateUrl<TDomain extends keyof TActualDef['endpoints'], TRouteKey extends keyof TActualDef['endpoints'][TDomain]>(domain: TDomain, routeKey: TRouteKey, params?: ApiClientParams<TActualDef, TDomain, TRouteKey>, query?: ApiClientQuery<TActualDef, TDomain, TRouteKey>): string;
131
143
  /**
132
144
  * Makes an API call to a specified domain and route.
133
145
  * @template TDomain The domain (controller) of the API.
package/dist/client.js CHANGED
@@ -88,6 +88,42 @@ class ApiClient {
88
88
  }
89
89
  return this.baseUrl;
90
90
  }
91
+ /**
92
+ * Generates the full URL for a specific route, incorporating path parameters and query parameters.
93
+ * @template TDomain The domain (controller) of the API.
94
+ * @template TRouteKey The key of the route within the domain.
95
+ * @param domain The API domain (e.g., 'user').
96
+ * @param routeKey The API route key (e.g., 'getUsers').
97
+ * @param params Optional path parameters to replace in the route path.
98
+ * @param query Optional query parameters to append to the URL.
99
+ * @returns The full URL as a string.
100
+ * @throws Error if the route configuration is invalid.
101
+ */
102
+ generateUrl(domain, routeKey, params, query) {
103
+ const routeInfo = this.apiDefinitionObject.endpoints[domain][routeKey];
104
+ if (!routeInfo || typeof routeInfo.path !== 'string') {
105
+ throw new Error(`API route configuration ${String(domain)}.${String(routeKey)} not found or invalid.`);
106
+ }
107
+ let urlPath = routeInfo.path;
108
+ if (params) {
109
+ const paramsRecord = params;
110
+ for (const key in paramsRecord) {
111
+ if (Object.prototype.hasOwnProperty.call(paramsRecord, key) && paramsRecord[key] !== undefined) {
112
+ urlPath = urlPath.replace(`:${key}`, String(paramsRecord[key]));
113
+ }
114
+ }
115
+ }
116
+ const url = new URL(this.getBaseUrlWithPrefix() + urlPath);
117
+ if (query) {
118
+ const queryRecord = query;
119
+ for (const key in queryRecord) {
120
+ if (Object.prototype.hasOwnProperty.call(queryRecord, key) && queryRecord[key] !== undefined) {
121
+ url.searchParams.append(key, String(queryRecord[key]));
122
+ }
123
+ }
124
+ }
125
+ return url.toString();
126
+ }
91
127
  /**
92
128
  * Makes an API call to a specified domain and route.
93
129
  * @template TDomain The domain (controller) of the API.
package/dist/handler.js CHANGED
@@ -302,7 +302,7 @@ middlewares) {
302
302
  baseUrl: expressReq.baseUrl,
303
303
  url: expressReq.url,
304
304
  };
305
- // Augment expressRes with the .respond method, using TDef
305
+ // Augment expressRes with the .respond and .setHeader methods, using TDef
306
306
  const typedExpressRes = expressRes;
307
307
  typedExpressRes.respond = (status, dataForResponse) => {
308
308
  // Use the passed apiDefinition object
@@ -347,6 +347,11 @@ middlewares) {
347
347
  });
348
348
  }
349
349
  };
350
+ typedExpressRes.setHeader = (name, value) => {
351
+ // Call the original Express setHeader method to avoid recursion
352
+ Object.getPrototypeOf(expressRes).setHeader.call(expressRes, name, value);
353
+ return typedExpressRes;
354
+ };
350
355
  const specificHandlerFn = handler;
351
356
  await specificHandlerFn(finalTypedReq, typedExpressRes);
352
357
  }
@@ -329,7 +329,11 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
329
329
  originalUrl: c.req.url
330
330
  };
331
331
  const fakeRes = {
332
- respond: c.respond
332
+ respond: c.respond,
333
+ setHeader: (name, value) => {
334
+ c.header(name, value);
335
+ return fakeRes;
336
+ }
333
337
  };
334
338
  const specificHandlerFn = handler;
335
339
  await specificHandlerFn(fakeReq, fakeRes);
package/dist/router.d.ts CHANGED
@@ -14,6 +14,7 @@ type ResponseDataForStatus<TDef extends ApiDefinitionSchema, TDomain extends key
14
14
  type RespondFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain]> = <TStatusLocal extends keyof TDef['endpoints'][TDomain][TRouteName]['responses'] & number>(status: TStatusLocal, data: ResponseDataForStatus<TDef, TDomain, TRouteName, TStatusLocal>) => void;
15
15
  export interface TypedResponse<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain], L extends Record<string, any> = Record<string, any>> extends express.Response<any, L> {
16
16
  respond: RespondFunction<TDef, TDomain, TRouteName>;
17
+ setHeader: (name: string, value: string) => this;
17
18
  json: <B = any>(body: B) => this;
18
19
  }
19
20
  export declare function createRouteHandler<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain], // Using direct keyof for simplicity
@@ -34,6 +34,15 @@ export const PublicApiDefinition = CreateApiDefinition({
34
34
  200: z.enum(["pong"]),
35
35
  })
36
36
  },
37
+ customHeaders: {
38
+ method: 'GET',
39
+ path: '/custom-headers',
40
+ responses: CreateResponses({
41
+ 200: z.object({
42
+ message: z.string()
43
+ }),
44
+ })
45
+ },
37
46
  }
38
47
  }
39
48
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-typed-api",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "A lightweight, type-safe RPC library for TypeScript with Zod validation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/client.ts CHANGED
@@ -216,6 +216,56 @@ export class ApiClient<TActualDef extends BaseApiDefinitionSchema> { // Made gen
216
216
  return this.baseUrl;
217
217
  }
218
218
 
219
+ /**
220
+ * Generates the full URL for a specific route, incorporating path parameters and query parameters.
221
+ * @template TDomain The domain (controller) of the API.
222
+ * @template TRouteKey The key of the route within the domain.
223
+ * @param domain The API domain (e.g., 'user').
224
+ * @param routeKey The API route key (e.g., 'getUsers').
225
+ * @param params Optional path parameters to replace in the route path.
226
+ * @param query Optional query parameters to append to the URL.
227
+ * @returns The full URL as a string.
228
+ * @throws Error if the route configuration is invalid.
229
+ */
230
+ public generateUrl<
231
+ TDomain extends keyof TActualDef['endpoints'],
232
+ TRouteKey extends keyof TActualDef['endpoints'][TDomain]
233
+ >(
234
+ domain: TDomain,
235
+ routeKey: TRouteKey,
236
+ params?: ApiClientParams<TActualDef, TDomain, TRouteKey>,
237
+ query?: ApiClientQuery<TActualDef, TDomain, TRouteKey>
238
+ ): string {
239
+ const routeInfo = this.apiDefinitionObject.endpoints[domain as string][routeKey as string] as RouteSchema;
240
+
241
+ if (!routeInfo || typeof routeInfo.path !== 'string') {
242
+ throw new Error(`API route configuration ${String(domain)}.${String(routeKey)} not found or invalid.`);
243
+ }
244
+
245
+ let urlPath = routeInfo.path;
246
+ if (params) {
247
+ const paramsRecord = params as Record<string, string | number | boolean>;
248
+ for (const key in paramsRecord) {
249
+ if (Object.prototype.hasOwnProperty.call(paramsRecord, key) && paramsRecord[key] !== undefined) {
250
+ urlPath = urlPath.replace(`:${key}`, String(paramsRecord[key]));
251
+ }
252
+ }
253
+ }
254
+
255
+ const url = new URL(this.getBaseUrlWithPrefix() + urlPath);
256
+
257
+ if (query) {
258
+ const queryRecord = query as Record<string, any>;
259
+ for (const key in queryRecord) {
260
+ if (Object.prototype.hasOwnProperty.call(queryRecord, key) && queryRecord[key] !== undefined) {
261
+ url.searchParams.append(key, String(queryRecord[key]));
262
+ }
263
+ }
264
+ }
265
+
266
+ return url.toString();
267
+ }
268
+
219
269
  /**
220
270
  * Makes an API call to a specified domain and route.
221
271
  * @template TDomain The domain (controller) of the API.
package/src/handler.ts CHANGED
@@ -355,7 +355,7 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
355
355
  url: expressReq.url,
356
356
  } as TypedRequest<TDef, typeof currentDomain, typeof currentRouteKey>;
357
357
 
358
- // Augment expressRes with the .respond method, using TDef
358
+ // Augment expressRes with the .respond and .setHeader methods, using TDef
359
359
  const typedExpressRes = expressRes as TypedResponse<TDef, typeof currentDomain, typeof currentRouteKey>;
360
360
 
361
361
  typedExpressRes.respond = (status, dataForResponse) => {
@@ -408,6 +408,12 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
408
408
  }
409
409
  };
410
410
 
411
+ typedExpressRes.setHeader = (name: string, value: string) => {
412
+ // Call the original Express setHeader method to avoid recursion
413
+ Object.getPrototypeOf(expressRes).setHeader.call(expressRes, name, value);
414
+ return typedExpressRes;
415
+ };
416
+
411
417
  const specificHandlerFn = handler as (
412
418
  req: TypedRequest<TDef, typeof currentDomain, typeof currentRouteKey>,
413
419
  res: TypedResponse<TDef, typeof currentDomain, typeof currentRouteKey>
@@ -407,7 +407,11 @@ export function registerHonoRouteHandlers<
407
407
  } as TypedRequest<TDef, typeof currentDomain, typeof currentRouteKey>;
408
408
 
409
409
  const fakeRes = {
410
- respond: (c as any).respond
410
+ respond: (c as any).respond,
411
+ setHeader: (name: string, value: string) => {
412
+ c.header(name, value);
413
+ return fakeRes;
414
+ }
411
415
  } as TypedResponse<TDef, typeof currentDomain, typeof currentRouteKey>;
412
416
 
413
417
  const specificHandlerFn = handler as (
package/src/router.ts CHANGED
@@ -59,6 +59,7 @@ export interface TypedResponse<
59
59
  L extends Record<string, any> = Record<string, any>
60
60
  > extends express.Response<any, L> {
61
61
  respond: RespondFunction<TDef, TDomain, TRouteName>;
62
+ setHeader: (name: string, value: string) => this;
62
63
  json: <B = any>(body: B) => this; // Keep original json
63
64
  }
64
65
 
package/tests/setup.ts CHANGED
@@ -14,6 +14,11 @@ const simplePublicHandlers = {
14
14
  common: {
15
15
  ping: async (req: any, res: any) => {
16
16
  res.respond(200, "pong");
17
+ },
18
+ customHeaders: async (req: any, res: any) => {
19
+ res.setHeader('X-Custom-Test', 'test-value');
20
+ res.setHeader('X-Another-Header', 'another-value');
21
+ res.respond(200, { message: "headers set" });
17
22
  }
18
23
  },
19
24
  status: {
@@ -74,6 +74,59 @@ describe.each([
74
74
 
75
75
  expect(result).toBe('pong');
76
76
  });
77
+
78
+ test('should set response headers', async () => {
79
+ // Direct HTTP call to test header setting
80
+ const response = await fetch(`${baseUrl}/api/v1/public/ping`);
81
+ const contentType = response.headers.get('content-type');
82
+
83
+ // Check that standard headers are set
84
+ if (serverName === 'Express') {
85
+ expect(contentType).toBe('application/json; charset=utf-8');
86
+ } else {
87
+ // Hono sets content-type differently
88
+ expect(contentType).toBe('application/json');
89
+ }
90
+ expect(response.status).toBe(200);
91
+ });
92
+
93
+ test('should set custom headers', async () => {
94
+ // Direct HTTP call to test custom header setting
95
+ const response = await fetch(`${baseUrl}/api/v1/public/custom-headers`);
96
+ const data = await response.json();
97
+
98
+ // Check response data (unified response format)
99
+ expect(data.data).toEqual({ message: "headers set" });
100
+ expect(data).not.toHaveProperty('error'); // Error should not be present for success responses
101
+ expect(response.status).toBe(200);
102
+
103
+ // Check custom headers
104
+ const customHeader = response.headers.get('x-custom-test');
105
+ const anotherHeader = response.headers.get('x-another-header');
106
+
107
+ expect(customHeader).toBe('test-value');
108
+ expect(anotherHeader).toBe('another-value');
109
+ });
110
+
111
+ test('generateUrl should return correct URL for ping', () => {
112
+ const url = client.generateUrl('common', 'ping');
113
+ expect(url).toBe(`${baseUrl}/api/v1/public/ping`);
114
+ });
115
+
116
+ test('generateUrl should return correct URL for probe1 without query', () => {
117
+ const url = client.generateUrl('status', 'probe1');
118
+ expect(url).toBe(`${baseUrl}/api/v1/public/status/probe1`);
119
+ });
120
+
121
+ test('generateUrl should return correct URL for probe1 with query', () => {
122
+ const url = client.generateUrl('status', 'probe1', undefined, { match: true });
123
+ expect(url).toBe(`${baseUrl}/api/v1/public/status/probe1?match=true`);
124
+ });
125
+
126
+ test('generateUrl should return correct URL for probe2', () => {
127
+ const url = client.generateUrl('status', 'probe2');
128
+ expect(url).toBe(`${baseUrl}/api/v1/public/status/probe2`);
129
+ });
77
130
  });
78
131
 
79
132
  describe('Private API', () => {
@@ -94,6 +147,11 @@ describe.each([
94
147
 
95
148
  expect(result).toBe('ok');
96
149
  });
150
+
151
+ test('generateUrl should return correct URL for user get with params', () => {
152
+ const url = client.generateUrl('user', 'get', { id: 'test-id' });
153
+ expect(url).toBe(`${baseUrl}/api/v1/private/user/test-id`);
154
+ });
97
155
  });
98
156
 
99
157
  describe('Strict Validation Tests', () => {