ts-typed-api 0.1.21 → 0.1.23

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.
@@ -0,0 +1,552 @@
1
+ import { Hono, Context, MiddlewareHandler } from 'hono';
2
+ import { z } from 'zod';
3
+ import { ApiDefinitionSchema, RouteSchema, UnifiedError, FileUploadConfig } from './definition';
4
+ import { TypedRequest, TypedResponse } from './router';
5
+ import { SpecificRouteHandler } from './handler';
6
+ import { ObjectHandlers, AnyMiddleware, EndpointMiddleware, SimpleMiddleware } from './object-handlers';
7
+
8
+ // Hono-specific file type for Cloudflare Workers
9
+ export type HonoFile = File;
10
+
11
+ // Hono-compatible file schema for Workers environment
12
+ export const honoFileSchema = z.object({
13
+ fieldname: z.string(),
14
+ originalname: z.string(),
15
+ encoding: z.string(),
16
+ mimetype: z.string(),
17
+ size: z.number(),
18
+ buffer: z.instanceof(Uint8Array).optional(), // Workers use Uint8Array instead of Buffer
19
+ file: z.instanceof(File).optional(), // Direct File object access
20
+ destination: z.string().optional(),
21
+ filename: z.string().optional(),
22
+ path: z.string().optional(),
23
+ stream: z.any().optional(),
24
+ });
25
+
26
+ export type HonoFileType = z.infer<typeof honoFileSchema>;
27
+
28
+ // Typed Hono Context that matches our Express-like API
29
+ export type HonoTypedContext<
30
+ TDef extends ApiDefinitionSchema,
31
+ TDomain extends keyof TDef['endpoints'],
32
+ TRouteKey extends keyof TDef['endpoints'][TDomain]
33
+ > = Context & {
34
+ // Add typed request properties
35
+ params: TypedRequest<TDef, TDomain, TRouteKey>['params'];
36
+ query: TypedRequest<TDef, TDomain, TRouteKey>['query'];
37
+ body: TypedRequest<TDef, TDomain, TRouteKey>['body'];
38
+ file?: HonoFile;
39
+ files?: HonoFile[] | { [fieldname: string]: HonoFile[] };
40
+
41
+ // Add typed response method
42
+ respond: TypedResponse<TDef, TDomain, TRouteKey>['respond'];
43
+ };
44
+
45
+ // Helper function to preprocess query parameters for type coercion
46
+ function preprocessQueryParams(query: any, querySchema?: z.ZodTypeAny): any {
47
+ if (!querySchema || !query) return query;
48
+
49
+ const processedQuery = { ...query };
50
+
51
+ if (querySchema instanceof z.ZodObject) {
52
+ const shape = querySchema.shape;
53
+
54
+ for (const [key, value] of Object.entries(processedQuery)) {
55
+ if (typeof value === 'string' && shape[key]) {
56
+ const fieldSchema = shape[key];
57
+
58
+ let innerSchema = fieldSchema;
59
+ if (fieldSchema instanceof z.ZodOptional) {
60
+ innerSchema = fieldSchema._def.innerType;
61
+ }
62
+ if (fieldSchema instanceof z.ZodDefault) {
63
+ innerSchema = fieldSchema._def.innerType;
64
+ }
65
+
66
+ while (innerSchema instanceof z.ZodOptional || innerSchema instanceof z.ZodDefault) {
67
+ if (innerSchema instanceof z.ZodOptional) {
68
+ innerSchema = innerSchema._def.innerType;
69
+ } else if (innerSchema instanceof z.ZodDefault) {
70
+ innerSchema = innerSchema._def.innerType;
71
+ }
72
+ }
73
+
74
+ if (innerSchema instanceof z.ZodNumber) {
75
+ const numValue = Number(value);
76
+ if (!isNaN(numValue)) {
77
+ processedQuery[key] = numValue;
78
+ }
79
+ } else if (innerSchema instanceof z.ZodBoolean) {
80
+ if (value === 'true') {
81
+ processedQuery[key] = true;
82
+ } else if (value === 'false') {
83
+ processedQuery[key] = false;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ return processedQuery;
91
+ }
92
+
93
+ // Helper function to create file upload middleware for Hono/Workers
94
+ function createHonoFileUploadMiddleware(config: FileUploadConfig): MiddlewareHandler {
95
+ return async (c: any, next: any) => {
96
+ try {
97
+ if (config.single) {
98
+ const formData = await c.req.parseBody({ all: false });
99
+ const file = formData[config.single.fieldName];
100
+
101
+ if (file instanceof File) {
102
+ // Validate file
103
+ if (config.single.maxSize && file.size > config.single.maxSize) {
104
+ return c.json({
105
+ data: null,
106
+ error: [{ field: 'file', message: `File size exceeds ${config.single.maxSize} bytes`, type: 'body' }]
107
+ }, 422);
108
+ }
109
+
110
+ if (config.single.allowedMimeTypes && !config.single.allowedMimeTypes.includes(file.type)) {
111
+ return c.json({
112
+ data: null,
113
+ error: [{ field: 'file', message: `File type ${file.type} not allowed`, type: 'body' }]
114
+ }, 422);
115
+ }
116
+
117
+ // Attach file to context
118
+ (c as any).file = {
119
+ fieldname: config.single.fieldName,
120
+ originalname: file.name,
121
+ encoding: '7bit',
122
+ mimetype: file.type,
123
+ size: file.size,
124
+ buffer: new Uint8Array(await file.arrayBuffer()),
125
+ file: file
126
+ };
127
+ }
128
+ } else if (config.array) {
129
+ const formData = await c.req.parseBody({ all: true });
130
+ const files = formData[config.array.fieldName];
131
+
132
+ if (Array.isArray(files)) {
133
+ // Validate files
134
+ if (config.array.maxCount && files.length > config.array.maxCount) {
135
+ return c.json({
136
+ data: null,
137
+ error: [{ field: 'file', message: `Maximum ${config.array.maxCount} files allowed`, type: 'body' }]
138
+ }, 422);
139
+ }
140
+
141
+ const processedFiles = [];
142
+ for (const file of files) {
143
+ if (file instanceof File) {
144
+ if (config.array.maxSize && file.size > config.array.maxSize) {
145
+ return c.json({
146
+ data: null,
147
+ error: [{ field: 'file', message: `File size exceeds ${config.array.maxSize} bytes`, type: 'body' }]
148
+ }, 422);
149
+ }
150
+
151
+ if (config.array.allowedMimeTypes && !config.array.allowedMimeTypes.includes(file.type)) {
152
+ return c.json({
153
+ data: null,
154
+ error: [{ field: 'file', message: `File type ${file.type} not allowed`, type: 'body' }]
155
+ }, 422);
156
+ }
157
+
158
+ processedFiles.push({
159
+ fieldname: config.array.fieldName,
160
+ originalname: file.name,
161
+ encoding: '7bit',
162
+ mimetype: file.type,
163
+ size: file.size,
164
+ buffer: new Uint8Array(await file.arrayBuffer()),
165
+ file: file
166
+ });
167
+ }
168
+ }
169
+
170
+ (c as any).files = processedFiles;
171
+ }
172
+ } else if (config.fields) {
173
+ const formData = await c.req.parseBody({ all: true });
174
+ const filesMap: { [fieldname: string]: HonoFileType[] } = {};
175
+
176
+ for (const fieldConfig of config.fields) {
177
+ const files = formData[fieldConfig.fieldName];
178
+ if (Array.isArray(files)) {
179
+ if (fieldConfig.maxCount && files.length > fieldConfig.maxCount) {
180
+ return c.json({
181
+ data: null,
182
+ error: [{ field: fieldConfig.fieldName, message: `Maximum ${fieldConfig.maxCount} files allowed`, type: 'body' }]
183
+ }, 422);
184
+ }
185
+
186
+ const processedFiles = [];
187
+ for (const file of files) {
188
+ if (file instanceof File) {
189
+ if (fieldConfig.maxSize && file.size > fieldConfig.maxSize) {
190
+ return c.json({
191
+ data: null,
192
+ error: [{ field: fieldConfig.fieldName, message: `File size exceeds ${fieldConfig.maxSize} bytes`, type: 'body' }]
193
+ }, 422);
194
+ }
195
+
196
+ if (fieldConfig.allowedMimeTypes && !fieldConfig.allowedMimeTypes.includes(file.type)) {
197
+ return c.json({
198
+ data: null,
199
+ error: [{ field: fieldConfig.fieldName, message: `File type ${file.type} not allowed`, type: 'body' }]
200
+ }, 422);
201
+ }
202
+
203
+ processedFiles.push({
204
+ fieldname: fieldConfig.fieldName,
205
+ originalname: file.name,
206
+ encoding: '7bit',
207
+ mimetype: file.type,
208
+ size: file.size,
209
+ buffer: new Uint8Array(await file.arrayBuffer()),
210
+ file: file
211
+ });
212
+ }
213
+ }
214
+
215
+ filesMap[fieldConfig.fieldName] = processedFiles;
216
+ }
217
+ }
218
+
219
+ (c as any).files = filesMap;
220
+ } else if (config.any) {
221
+ const formData = await c.req.parseBody({ all: true });
222
+
223
+ // Process all files
224
+ const allFiles: HonoFileType[] = [];
225
+ for (const [key, value] of Object.entries(formData)) {
226
+ if (value instanceof File) {
227
+ if (config.any.maxSize && value.size > config.any.maxSize) {
228
+ return c.json({
229
+ data: null,
230
+ error: [{ field: key, message: `File size exceeds ${config.any.maxSize} bytes`, type: 'body' }]
231
+ }, 422);
232
+ }
233
+
234
+ if (config.any.allowedMimeTypes && !config.any.allowedMimeTypes.includes(value.type)) {
235
+ return c.json({
236
+ data: null,
237
+ error: [{ field: key, message: `File type ${value.type} not allowed`, type: 'body' }]
238
+ }, 422);
239
+ }
240
+
241
+ allFiles.push({
242
+ fieldname: key,
243
+ originalname: value.name,
244
+ encoding: '7bit',
245
+ mimetype: value.type,
246
+ size: value.size,
247
+ buffer: new Uint8Array(await value.arrayBuffer()),
248
+ file: value
249
+ });
250
+ }
251
+ }
252
+
253
+ (c as any).files = allFiles;
254
+ }
255
+
256
+ await next();
257
+ } catch (error) {
258
+ console.error('File upload middleware error:', error);
259
+ return c.json({
260
+ data: null,
261
+ error: [{ field: 'file', message: 'File upload processing failed', type: 'body' }]
262
+ }, 422);
263
+ }
264
+ };
265
+ }
266
+
267
+ // Register route handlers with Hono, now generic over TDef
268
+ export function registerHonoRouteHandlers<TDef extends ApiDefinitionSchema>(
269
+ app: Hono,
270
+ apiDefinition: TDef,
271
+ routeHandlers: Array<SpecificRouteHandler<TDef>>,
272
+ middlewares?: EndpointMiddleware<TDef>[]
273
+ ) {
274
+ routeHandlers.forEach((specificHandlerIterationItem) => {
275
+ const { domain, routeKey, handler } = specificHandlerIterationItem as any;
276
+
277
+ const currentDomain = domain as string;
278
+ const currentRouteKey = routeKey as string;
279
+
280
+ const routeDefinition = apiDefinition.endpoints[currentDomain][currentRouteKey] as RouteSchema;
281
+
282
+ if (!routeDefinition) {
283
+ console.error(`Route definition not found for domain "${String(currentDomain)}" and routeKey "${String(currentRouteKey)}"`);
284
+ return;
285
+ }
286
+
287
+ const { path, method } = routeDefinition;
288
+
289
+ // Apply prefix from API definition if it exists
290
+ const fullPath = apiDefinition.prefix
291
+ ? `${apiDefinition.prefix.startsWith('/') ? apiDefinition.prefix : `/${apiDefinition.prefix}`}${path}`.replace(/\/+/g, '/')
292
+ : path;
293
+
294
+ const honoMiddleware = async (
295
+ c: HonoTypedContext<TDef, typeof currentDomain, typeof currentRouteKey>
296
+ ) => {
297
+ try {
298
+ // Parse and validate request
299
+ const parsedParams = ('params' in routeDefinition && routeDefinition.params)
300
+ ? (routeDefinition.params as z.ZodTypeAny).parse(c.req.param())
301
+ : c.req.param();
302
+
303
+ const preprocessedQuery = ('query' in routeDefinition && routeDefinition.query)
304
+ ? preprocessQueryParams(c.req.query(), routeDefinition.query as z.ZodTypeAny)
305
+ : c.req.query();
306
+
307
+ const parsedQuery = ('query' in routeDefinition && routeDefinition.query)
308
+ ? (routeDefinition.query as z.ZodTypeAny).parse(preprocessedQuery)
309
+ : preprocessedQuery;
310
+
311
+ let parsedBody: any = undefined;
312
+ if (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH') {
313
+ if ('body' in routeDefinition && routeDefinition.body) {
314
+ // For JSON requests
315
+ if (c.req.header('content-type')?.includes('application/json')) {
316
+ parsedBody = (routeDefinition.body as z.ZodTypeAny).parse(await c.req.json());
317
+ } else {
318
+ // For form data or other body types
319
+ parsedBody = (routeDefinition.body as z.ZodTypeAny).parse(await c.req.parseBody());
320
+ }
321
+ } else {
322
+ parsedBody = await c.req.parseBody();
323
+ }
324
+ }
325
+
326
+ // Attach parsed data to context
327
+ (c as any).params = parsedParams;
328
+ (c as any).query = parsedQuery;
329
+ (c as any).body = parsedBody;
330
+
331
+ // Add respond method to context
332
+ (c as any).respond = (status: number, data: any) => {
333
+ const responseSchema = routeDefinition.responses[status];
334
+
335
+ if (!responseSchema) {
336
+ console.error(`No response schema defined for status ${status} in route ${String(currentDomain)}/${String(currentRouteKey)}`);
337
+ (c as any).__response = c.json({
338
+ data: null,
339
+ error: [{ field: "general", type: "general", message: "Internal server error: Undefined response schema for status." }]
340
+ }, 500);
341
+ return;
342
+ }
343
+
344
+ let responseBody: any;
345
+
346
+ if (status === 422) {
347
+ responseBody = {
348
+ data: null,
349
+ error: data
350
+ };
351
+ } else {
352
+ // Always use unified response format since CreateResponses wraps all schemas
353
+ responseBody = {
354
+ data: data,
355
+ error: null
356
+ };
357
+ }
358
+
359
+ const validationResult = responseSchema.safeParse(responseBody);
360
+
361
+ if (validationResult.success) {
362
+ // Handle 204 responses specially - they must not have a body
363
+ if (status === 204) {
364
+ (c as any).__response = new Response(null, { status: status as any });
365
+ } else {
366
+ (c as any).__response = c.json(validationResult.data, status as any);
367
+ }
368
+ } else {
369
+ console.error(
370
+ `FATAL: Constructed response body failed Zod validation for status ${status} in route ${String(currentDomain)}/${String(currentRouteKey)}.`,
371
+ validationResult.error.issues,
372
+ 'Expected schema shape:', (responseSchema._def as any)?.shape,
373
+ 'Provided data:', data,
374
+ 'Constructed response body:', responseBody
375
+ );
376
+ (c as any).__response = c.json({
377
+ data: null,
378
+ error: [{ field: "general", type: "general", message: "Internal server error: Constructed response failed validation." }]
379
+ }, 500);
380
+ }
381
+ };
382
+
383
+ // Create Express-like req/res objects for handler compatibility
384
+ const fakeReq = {
385
+ params: parsedParams,
386
+ query: parsedQuery,
387
+ body: parsedBody,
388
+ file: (c as any).file,
389
+ files: (c as any).files,
390
+ headers: c.req.header(),
391
+ ip: c.req.header('CF-Connecting-IP') || '127.0.0.1',
392
+ method: c.req.method,
393
+ path: c.req.path,
394
+ originalUrl: c.req.url
395
+ } as TypedRequest<TDef, typeof currentDomain, typeof currentRouteKey>;
396
+
397
+ const fakeRes = {
398
+ respond: (c as any).respond
399
+ } as TypedResponse<TDef, typeof currentDomain, typeof currentRouteKey>;
400
+
401
+ const specificHandlerFn = handler as (
402
+ req: TypedRequest<TDef, typeof currentDomain, typeof currentRouteKey>,
403
+ res: TypedResponse<TDef, typeof currentDomain, typeof currentRouteKey>
404
+ ) => Promise<void> | void;
405
+
406
+ await specificHandlerFn(fakeReq, fakeRes);
407
+
408
+ // Return the response created by the handler
409
+ if ((c as any).__response) {
410
+ return (c as any).__response;
411
+ } else {
412
+ console.error('No response was set by the handler');
413
+ return c.json({
414
+ data: null,
415
+ error: [{ field: "general", type: "general", message: "Internal server error: No response set by handler." }]
416
+ }, 500);
417
+ }
418
+
419
+ } catch (error) {
420
+ if (error instanceof z.ZodError) {
421
+ const mappedErrors: UnifiedError = error.issues.map(err => {
422
+ let errorType: 'param' | 'query' | 'body' | 'general' = 'general';
423
+ const pathZero = String(err.path[0]);
424
+ if (pathZero === 'params') errorType = 'param';
425
+ else if (pathZero === 'query') errorType = 'query';
426
+ else if (pathZero === 'body') errorType = 'body';
427
+
428
+ return {
429
+ field: err.path.join('.') || 'request',
430
+ message: err.message,
431
+ type: errorType,
432
+ };
433
+ });
434
+
435
+ const errorResponseBody = { data: null, error: mappedErrors };
436
+ const schema422 = routeDefinition.responses[422];
437
+
438
+ if (schema422) {
439
+ const validationResult = schema422.safeParse(errorResponseBody);
440
+ if (validationResult.success) {
441
+ return c.json(validationResult.data, 422);
442
+ } else {
443
+ console.error("FATAL: Constructed 422 error response failed its own schema validation.", validationResult.error.issues);
444
+ return c.json({ error: [{ field: "general", type: "general", message: "Internal server error constructing validation error response." }] }, 500);
445
+ }
446
+ } else {
447
+ console.error("Error: 422 schema not found for route, sending raw Zod errors.");
448
+ return c.json({ error: mappedErrors }, 422);
449
+ }
450
+ } else if (error instanceof Error) {
451
+ console.error(`Error in ${method} ${path}:`, error.message);
452
+ return c.json({ error: [{ field: "general", type: "general", message: 'Internal server error' }] }, 500);
453
+ } else {
454
+ console.error(`Unknown error in ${method} ${path}:`, error);
455
+ return c.json({ error: [{ field: "general", type: "general", message: 'An unknown error occurred' }] }, 500);
456
+ }
457
+ }
458
+ };
459
+
460
+ // Create middleware wrappers
461
+ const middlewareWrappers: MiddlewareHandler[] = [];
462
+
463
+ // Add file upload middleware if configured
464
+ if (routeDefinition.fileUpload) {
465
+ try {
466
+ const fileUploadMiddleware = createHonoFileUploadMiddleware(routeDefinition.fileUpload);
467
+ middlewareWrappers.push(fileUploadMiddleware);
468
+ } catch (error) {
469
+ console.error(`Error creating file upload middleware for ${currentDomain}.${currentRouteKey}:`, error);
470
+ return;
471
+ }
472
+ }
473
+
474
+ if (middlewares && middlewares.length > 0) {
475
+ middlewares.forEach(middleware => {
476
+ const wrappedMiddleware: MiddlewareHandler = async (c: any, next: any) => {
477
+ try {
478
+ await middleware(c.req as any, c.res as any, next, { domain: currentDomain, routeKey: currentRouteKey } as any);
479
+ } catch (error) {
480
+ console.error('Middleware error:', error);
481
+ return next();
482
+ }
483
+ };
484
+ middlewareWrappers.push(wrappedMiddleware);
485
+ });
486
+ }
487
+
488
+ // Register route with middlewares
489
+ const allHandlers = [...middlewareWrappers, honoMiddleware];
490
+
491
+ // Register with Hono
492
+ switch (method.toUpperCase()) {
493
+ case 'GET': app.get(fullPath, ...(allHandlers as any)); break;
494
+ case 'POST': app.post(fullPath, ...(allHandlers as any)); break;
495
+ case 'PATCH': app.patch(fullPath, ...(allHandlers as any)); break;
496
+ case 'PUT': app.put(fullPath, ...(allHandlers as any)); break;
497
+ case 'DELETE': app.delete(fullPath, ...(allHandlers as any)); break;
498
+ default:
499
+ console.warn(`Unsupported HTTP method: ${method} for path ${fullPath}`);
500
+ }
501
+ });
502
+ }
503
+
504
+ // Transform object-based handlers to array format
505
+ function transformObjectHandlersToArray<TDef extends ApiDefinitionSchema>(
506
+ objectHandlers: ObjectHandlers<TDef>
507
+ ): Array<SpecificRouteHandler<TDef>> {
508
+ const handlerArray: Array<SpecificRouteHandler<TDef>> = [];
509
+
510
+ for (const domain in objectHandlers) {
511
+ if (Object.prototype.hasOwnProperty.call(objectHandlers, domain)) {
512
+ const domainHandlers = objectHandlers[domain];
513
+
514
+ for (const routeKey in domainHandlers) {
515
+ if (Object.prototype.hasOwnProperty.call(domainHandlers, routeKey)) {
516
+ const handler = domainHandlers[routeKey];
517
+
518
+ handlerArray.push({
519
+ domain,
520
+ routeKey,
521
+ handler
522
+ } as SpecificRouteHandler<TDef>);
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ return handlerArray;
529
+ }
530
+
531
+ // Main utility function that registers object-based handlers with Hono
532
+ export function RegisterHonoHandlers<TDef extends ApiDefinitionSchema>(
533
+ app: Hono,
534
+ apiDefinition: TDef,
535
+ objectHandlers: ObjectHandlers<TDef>,
536
+ middlewares?: AnyMiddleware<TDef>[]
537
+ ): void {
538
+ const handlerArray = transformObjectHandlersToArray(objectHandlers);
539
+
540
+ // Convert AnyMiddleware to EndpointMiddleware by checking function arity
541
+ const endpointMiddlewares: EndpointMiddleware<TDef>[] = middlewares?.map(middleware => {
542
+ if (middleware.length === 4) {
543
+ return middleware as EndpointMiddleware<TDef>;
544
+ } else {
545
+ return ((req, res, next) => {
546
+ return (middleware as SimpleMiddleware)(req, res, next);
547
+ }) as EndpointMiddleware<TDef>;
548
+ }
549
+ }) || [];
550
+
551
+ registerHonoRouteHandlers(app, apiDefinition, handlerArray, endpointMiddlewares);
552
+ }
package/src/index.ts CHANGED
@@ -5,3 +5,6 @@ export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema } from './def
5
5
  export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo } from './object-handlers';
6
6
  export { File as UploadedFile } from './router';
7
7
  export { z as ZodSchema } from 'zod';
8
+
9
+ // Hono adapter for Cloudflare Workers
10
+ export { RegisterHonoHandlers, registerHonoRouteHandlers, HonoFile, HonoFileType, honoFileSchema, HonoTypedContext } from './hono-cloudflare-workers';
@@ -270,10 +270,50 @@ class SchemaRegistry {
270
270
  }
271
271
 
272
272
  if (zodSchema instanceof ZodArray) {
273
- const itemType = getZodType(zodSchema);
273
+ // Try multiple ways to get the array item type
274
+ let itemType: ZodTypeAny | undefined;
275
+
276
+ // Method 1: Try _def.element (this is the correct property for ZodArray)
277
+ try {
278
+ const def = getZodDef(zodSchema);
279
+ if (def && def.element && typeof def.element === 'object' && def.element.constructor) {
280
+ itemType = def.element;
281
+ }
282
+ } catch (error) {
283
+ // Continue to next method
284
+ }
285
+
286
+ // Method 2: Try getZodType if method 1 failed
287
+ if (!itemType) {
288
+ const typeResult = getZodType(zodSchema);
289
+ if (typeResult && typeof typeResult === 'object' && typeResult.constructor) {
290
+ itemType = typeResult;
291
+ }
292
+ }
293
+
294
+ // Method 3: Direct access to _def.element as fallback
295
+ if (!itemType) {
296
+ try {
297
+ const directElement = (zodSchema as any)._def?.element;
298
+ if (directElement && typeof directElement === 'object' && directElement.constructor) {
299
+ itemType = directElement;
300
+ }
301
+ } catch (error) {
302
+ // Continue to fallback
303
+ }
304
+ }
305
+
306
+ if (itemType) {
307
+ return {
308
+ type: 'array',
309
+ items: this.zodToOpenAPI(itemType, shouldRegister)
310
+ };
311
+ }
312
+
313
+ // Ultimate fallback
274
314
  return {
275
315
  type: 'array',
276
- items: itemType ? this.zodToOpenAPI(itemType, shouldRegister) : { type: 'string' }
316
+ items: { type: 'string' }
277
317
  };
278
318
  }
279
319
 
@@ -1,10 +1,17 @@
1
- import { describe, test, expect } from '@jest/globals';
1
+ import { describe, test, expect, beforeEach } from '@jest/globals';
2
2
  import { ApiClient } from '../src';
3
3
  import { PublicApiDefinition, PrivateApiDefinition } from '../examples/advanced/definitions';
4
- import { ADVANCED_PORT } from './setup';
4
+ import { ADVANCED_PORT, ADVANCED_HONO_PORT, resetMockData } from './setup';
5
5
 
6
- describe('Advanced API Tests', () => {
7
- const baseUrl = `http://localhost:${ADVANCED_PORT}`;
6
+ describe.each([
7
+ ['Express', ADVANCED_PORT],
8
+ ['Hono', ADVANCED_HONO_PORT]
9
+ ])('Advanced API Tests - %s', (serverName, port) => {
10
+ const baseUrl = `http://localhost:${port}`;
11
+
12
+ beforeEach(() => {
13
+ resetMockData();
14
+ });
8
15
 
9
16
  describe('Public API - Authentication', () => {
10
17
  const client = new ApiClient(baseUrl, PublicApiDefinition);