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 +12 -0
- package/dist/client.js +36 -0
- package/dist/handler.js +6 -1
- package/dist/hono-cloudflare-workers.js +5 -1
- package/dist/router.d.ts +1 -0
- package/examples/simple/definitions.ts +9 -0
- package/package.json +1 -1
- package/src/client.ts +50 -0
- package/src/handler.ts +7 -1
- package/src/hono-cloudflare-workers.ts +5 -1
- package/src/router.ts +1 -0
- package/tests/setup.ts +5 -0
- package/tests/simple-api.test.ts +58 -0
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
|
|
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
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
|
|
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: {
|
package/tests/simple-api.test.ts
CHANGED
|
@@ -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', () => {
|