ts-typed-api 0.1.0 → 0.1.2
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/README.md +58 -1
- package/dist/definition.d.ts +70 -0
- package/dist/definition.js +44 -1
- package/dist/handler.js +159 -0
- package/dist/index.d.ts +1 -0
- package/dist/router.d.ts +7 -1
- package/examples/file-upload-example.ts +158 -0
- package/package.json +3 -1
- package/src/definition.ts +97 -0
- package/src/handler.ts +159 -1
- package/src/index.ts +1 -0
- package/src/router.ts +8 -1
- package/dist/apiClient.d.ts +0 -147
- package/dist/apiClient.js +0 -206
- package/dist/example-server/client.d.ts +0 -2
- package/dist/example-server/client.js +0 -19
- package/dist/example-server/definitions.d.ts +0 -157
- package/dist/example-server/definitions.js +0 -35
- package/dist/example-server/server.d.ts +0 -1
- package/dist/example-server/server.js +0 -66
- package/dist/openapiGenerator.d.ts +0 -2
- package/dist/openapiGenerator.js +0 -119
- package/dist/router/definition.d.ts +0 -118
- package/dist/router/definition.js +0 -80
- package/dist/router/router.d.ts +0 -29
- package/dist/router/router.js +0 -168
- package/dist/src/router/client.d.ts +0 -151
- package/dist/src/router/client.js +0 -215
- package/dist/src/router/definition.d.ts +0 -121
- package/dist/src/router/definition.js +0 -79
- package/dist/src/router/handler.d.ts +0 -16
- package/dist/src/router/handler.js +0 -170
- package/dist/src/router/index.d.ts +0 -5
- package/dist/src/router/index.js +0 -22
- package/dist/src/router/object-handlers.d.ts +0 -16
- package/dist/src/router/object-handlers.js +0 -39
- package/dist/src/router/router.d.ts +0 -18
- package/dist/src/router/router.js +0 -20
package/src/definition.ts
CHANGED
|
@@ -95,6 +95,8 @@ export function CreateResponses<TInputMap extends Partial<Record<AllowedInputSta
|
|
|
95
95
|
});
|
|
96
96
|
} else if (schemaOrMarker instanceof ZodType) { // It's a Zod schema
|
|
97
97
|
(builtResult as any)[numericKey] = createSuccessUnifiedResponseSchema(schemaOrMarker);
|
|
98
|
+
} else {
|
|
99
|
+
(builtResult as any)[numericKey] = createSuccessUnifiedResponseSchema(schemaOrMarker);
|
|
98
100
|
}
|
|
99
101
|
// Note: If schemaOrMarker is something else, it would be a type error
|
|
100
102
|
// based on InputSchemaOrMarker, or this runtime check would skip it.
|
|
@@ -109,6 +111,35 @@ export function CreateResponses<TInputMap extends Partial<Record<AllowedInputSta
|
|
|
109
111
|
return builtResult as CreateResponsesReturnType<TInputMap>;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
// File upload configuration interface
|
|
115
|
+
export interface FileUploadConfig {
|
|
116
|
+
// Single file upload
|
|
117
|
+
single?: {
|
|
118
|
+
fieldName: string;
|
|
119
|
+
maxSize?: number; // in bytes
|
|
120
|
+
allowedMimeTypes?: string[];
|
|
121
|
+
};
|
|
122
|
+
// Multiple files upload (same field name)
|
|
123
|
+
array?: {
|
|
124
|
+
fieldName: string;
|
|
125
|
+
maxCount?: number;
|
|
126
|
+
maxSize?: number; // in bytes per file
|
|
127
|
+
allowedMimeTypes?: string[];
|
|
128
|
+
};
|
|
129
|
+
// Multiple files upload (different field names)
|
|
130
|
+
fields?: Array<{
|
|
131
|
+
fieldName: string;
|
|
132
|
+
maxCount?: number;
|
|
133
|
+
maxSize?: number; // in bytes per file
|
|
134
|
+
allowedMimeTypes?: string[];
|
|
135
|
+
}>;
|
|
136
|
+
// Any files upload
|
|
137
|
+
any?: {
|
|
138
|
+
maxSize?: number; // in bytes per file
|
|
139
|
+
allowedMimeTypes?: string[];
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
112
143
|
// Define the structure for a single API route
|
|
113
144
|
export interface RouteSchema {
|
|
114
145
|
path: string;
|
|
@@ -116,6 +147,7 @@ export interface RouteSchema {
|
|
|
116
147
|
params?: ZodTypeAny;
|
|
117
148
|
query?: ZodTypeAny;
|
|
118
149
|
body?: ZodTypeAny;
|
|
150
|
+
fileUpload?: FileUploadConfig; // Optional file upload configuration
|
|
119
151
|
responses: Record<number, ZodTypeAny>; // Maps HTTP status codes to Zod schemas
|
|
120
152
|
}
|
|
121
153
|
|
|
@@ -229,3 +261,68 @@ export type ApiClientQuery<
|
|
|
229
261
|
> = TDef['endpoints'][TDomain][TRouteName] extends { query: infer Q extends ZodTypeAny }
|
|
230
262
|
? z.input<Q> // Use z.input for the type expected by the client to send
|
|
231
263
|
: undefined;
|
|
264
|
+
|
|
265
|
+
// --- File Upload Validation Schemas ---
|
|
266
|
+
|
|
267
|
+
// Schema for validating uploaded files
|
|
268
|
+
export const fileSchema = z.object({
|
|
269
|
+
fieldname: z.string(),
|
|
270
|
+
originalname: z.string(),
|
|
271
|
+
encoding: z.string(),
|
|
272
|
+
mimetype: z.string(),
|
|
273
|
+
size: z.number(),
|
|
274
|
+
buffer: z.instanceof(Buffer),
|
|
275
|
+
destination: z.string().optional(),
|
|
276
|
+
filename: z.string().optional(),
|
|
277
|
+
path: z.string().optional(),
|
|
278
|
+
stream: z.any().optional(),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
export type FileType = z.infer<typeof fileSchema>;
|
|
282
|
+
|
|
283
|
+
// Helper function to create file validation schema with constraints
|
|
284
|
+
export function createFileValidationSchema(options?: {
|
|
285
|
+
maxSize?: number;
|
|
286
|
+
allowedMimeTypes?: string[];
|
|
287
|
+
required?: boolean;
|
|
288
|
+
}) {
|
|
289
|
+
let schema: z.ZodTypeAny = fileSchema;
|
|
290
|
+
|
|
291
|
+
if (options?.maxSize) {
|
|
292
|
+
schema = schema.refine(
|
|
293
|
+
(file: any) => file.size <= options.maxSize!,
|
|
294
|
+
{ message: `File size must be less than ${options.maxSize} bytes` }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (options?.allowedMimeTypes && options.allowedMimeTypes.length > 0) {
|
|
299
|
+
schema = schema.refine(
|
|
300
|
+
(file: any) => options.allowedMimeTypes!.includes(file.mimetype),
|
|
301
|
+
{ message: `File type must be one of: ${options.allowedMimeTypes.join(', ')}` }
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return options?.required === false ? schema.optional() : schema;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Helper function to create array of files validation schema
|
|
309
|
+
export function createFilesArrayValidationSchema(options?: {
|
|
310
|
+
maxCount?: number;
|
|
311
|
+
maxSize?: number;
|
|
312
|
+
allowedMimeTypes?: string[];
|
|
313
|
+
required?: boolean;
|
|
314
|
+
}) {
|
|
315
|
+
const singleFileSchema = createFileValidationSchema({
|
|
316
|
+
maxSize: options?.maxSize,
|
|
317
|
+
allowedMimeTypes: options?.allowedMimeTypes,
|
|
318
|
+
required: true, // Individual files in array are required
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
let schema = z.array(singleFileSchema);
|
|
322
|
+
|
|
323
|
+
if (options?.maxCount) {
|
|
324
|
+
schema = schema.max(options.maxCount, `Maximum ${options.maxCount} files allowed`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return options?.required === false ? schema.optional() : schema;
|
|
328
|
+
}
|
package/src/handler.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { ApiDefinitionSchema, RouteSchema, UnifiedError } from "./definition";
|
|
2
|
+
import { ApiDefinitionSchema, RouteSchema, UnifiedError, FileUploadConfig } from "./definition";
|
|
3
3
|
import { createRouteHandler, TypedRequest, TypedResponse } from "./router";
|
|
4
4
|
import express from "express";
|
|
5
|
+
import multer from "multer";
|
|
5
6
|
|
|
6
7
|
// A handler entry, now generic over TDef
|
|
7
8
|
export type SpecificRouteHandler<TDef extends ApiDefinitionSchema> = {
|
|
@@ -20,6 +21,151 @@ type EndpointMiddleware = (
|
|
|
20
21
|
endpointInfo: { domain: string; routeKey: string }
|
|
21
22
|
) => void | Promise<void>;
|
|
22
23
|
|
|
24
|
+
// Helper function to create multer middleware based on file upload configuration
|
|
25
|
+
function createFileUploadMiddleware(config: FileUploadConfig): express.RequestHandler {
|
|
26
|
+
// Default multer configuration
|
|
27
|
+
const storage = multer.memoryStorage(); // Store files in memory by default
|
|
28
|
+
|
|
29
|
+
let multerMiddleware: express.RequestHandler;
|
|
30
|
+
|
|
31
|
+
if (config.single) {
|
|
32
|
+
const upload = multer({
|
|
33
|
+
storage,
|
|
34
|
+
limits: {
|
|
35
|
+
fileSize: config.single.maxSize || 10 * 1024 * 1024, // Default 10MB
|
|
36
|
+
},
|
|
37
|
+
fileFilter: (req, file, cb) => {
|
|
38
|
+
if (config.single!.allowedMimeTypes && !config.single!.allowedMimeTypes.includes(file.mimetype)) {
|
|
39
|
+
cb(new Error(`File type ${file.mimetype} not allowed`));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
cb(null, true);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
multerMiddleware = upload.single(config.single.fieldName);
|
|
46
|
+
} else if (config.array) {
|
|
47
|
+
const upload = multer({
|
|
48
|
+
storage,
|
|
49
|
+
limits: {
|
|
50
|
+
fileSize: config.array.maxSize || 10 * 1024 * 1024, // Default 10MB per file
|
|
51
|
+
files: config.array.maxCount || 10, // Default max 10 files
|
|
52
|
+
},
|
|
53
|
+
fileFilter: (req, file, cb) => {
|
|
54
|
+
if (config.array!.allowedMimeTypes && !config.array!.allowedMimeTypes.includes(file.mimetype)) {
|
|
55
|
+
cb(new Error(`File type ${file.mimetype} not allowed`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
cb(null, true);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
multerMiddleware = upload.array(config.array.fieldName, config.array.maxCount);
|
|
62
|
+
} else if (config.fields) {
|
|
63
|
+
const upload = multer({
|
|
64
|
+
storage,
|
|
65
|
+
limits: {
|
|
66
|
+
fileSize: Math.max(...config.fields.map(f => f.maxSize || 10 * 1024 * 1024)), // Use max size from all fields
|
|
67
|
+
},
|
|
68
|
+
fileFilter: (req, file, cb) => {
|
|
69
|
+
const fieldConfig = config.fields!.find(f => f.fieldName === file.fieldname);
|
|
70
|
+
if (fieldConfig?.allowedMimeTypes && !fieldConfig.allowedMimeTypes.includes(file.mimetype)) {
|
|
71
|
+
cb(new Error(`File type ${file.mimetype} not allowed for field ${file.fieldname}`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
cb(null, true);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const fields = config.fields.map(f => ({ name: f.fieldName, maxCount: f.maxCount || 1 }));
|
|
78
|
+
multerMiddleware = upload.fields(fields);
|
|
79
|
+
} else if (config.any) {
|
|
80
|
+
const upload = multer({
|
|
81
|
+
storage,
|
|
82
|
+
limits: {
|
|
83
|
+
fileSize: config.any.maxSize || 10 * 1024 * 1024, // Default 10MB per file
|
|
84
|
+
},
|
|
85
|
+
fileFilter: (req, file, cb) => {
|
|
86
|
+
if (config.any!.allowedMimeTypes && !config.any!.allowedMimeTypes.includes(file.mimetype)) {
|
|
87
|
+
cb(new Error(`File type ${file.mimetype} not allowed`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
cb(null, true);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
multerMiddleware = upload.any();
|
|
94
|
+
} else {
|
|
95
|
+
// Fallback - should not reach here if config is valid
|
|
96
|
+
throw new Error('Invalid file upload configuration');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Wrap multer middleware with error handling to format errors as 422 JSON responses
|
|
100
|
+
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
101
|
+
multerMiddleware(req, res, (error) => {
|
|
102
|
+
if (error) {
|
|
103
|
+
// Convert multer errors to UnifiedError format
|
|
104
|
+
const mappedErrors: UnifiedError = [];
|
|
105
|
+
|
|
106
|
+
if (error instanceof multer.MulterError) {
|
|
107
|
+
let errorMessage = error.message;
|
|
108
|
+
let fieldName = 'file';
|
|
109
|
+
|
|
110
|
+
switch (error.code) {
|
|
111
|
+
case 'LIMIT_FILE_SIZE':
|
|
112
|
+
errorMessage = 'File size exceeds the allowed limit';
|
|
113
|
+
break;
|
|
114
|
+
case 'LIMIT_FILE_COUNT':
|
|
115
|
+
errorMessage = 'Too many files uploaded';
|
|
116
|
+
break;
|
|
117
|
+
case 'LIMIT_UNEXPECTED_FILE':
|
|
118
|
+
errorMessage = `Unexpected field: ${error.field}`;
|
|
119
|
+
fieldName = error.field || 'file';
|
|
120
|
+
break;
|
|
121
|
+
case 'LIMIT_FIELD_KEY':
|
|
122
|
+
errorMessage = 'Field name too long';
|
|
123
|
+
break;
|
|
124
|
+
case 'LIMIT_FIELD_VALUE':
|
|
125
|
+
errorMessage = 'Field value too long';
|
|
126
|
+
break;
|
|
127
|
+
case 'LIMIT_FIELD_COUNT':
|
|
128
|
+
errorMessage = 'Too many fields';
|
|
129
|
+
break;
|
|
130
|
+
case 'LIMIT_PART_COUNT':
|
|
131
|
+
errorMessage = 'Too many parts';
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
errorMessage = error.message || 'File upload error';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
mappedErrors.push({
|
|
138
|
+
field: fieldName,
|
|
139
|
+
message: errorMessage,
|
|
140
|
+
type: 'body',
|
|
141
|
+
});
|
|
142
|
+
} else if (error instanceof Error) {
|
|
143
|
+
// Handle custom errors from fileFilter
|
|
144
|
+
mappedErrors.push({
|
|
145
|
+
field: 'file',
|
|
146
|
+
message: error.message,
|
|
147
|
+
type: 'body',
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
mappedErrors.push({
|
|
151
|
+
field: 'file',
|
|
152
|
+
message: 'File upload error',
|
|
153
|
+
type: 'body',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Send 422 response with structured error format
|
|
158
|
+
res.status(422).json({
|
|
159
|
+
data: null,
|
|
160
|
+
error: mappedErrors
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
next();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
23
169
|
// Register route handlers with Express, now generic over TDef
|
|
24
170
|
export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
25
171
|
app: express.Express,
|
|
@@ -192,6 +338,18 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
192
338
|
|
|
193
339
|
// Create middleware wrappers that include endpoint information
|
|
194
340
|
const middlewareWrappers: express.RequestHandler[] = [];
|
|
341
|
+
|
|
342
|
+
// Add file upload middleware if configured
|
|
343
|
+
if (routeDefinition.fileUpload) {
|
|
344
|
+
try {
|
|
345
|
+
const fileUploadMiddleware = createFileUploadMiddleware(routeDefinition.fileUpload);
|
|
346
|
+
middlewareWrappers.push(fileUploadMiddleware);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error(`Error creating file upload middleware for ${currentDomain}.${currentRouteKey}:`, error);
|
|
349
|
+
return; // Skip this route if file upload middleware creation fails
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
195
353
|
if (middlewares && middlewares.length > 0) {
|
|
196
354
|
middlewares.forEach(middleware => {
|
|
197
355
|
const wrappedMiddleware: express.RequestHandler = async (req, res, next) => {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ApiClient, FetchHttpClientAdapter } from './client';
|
|
2
2
|
export { CreateApiDefinition, CreateResponses } from './definition';
|
|
3
3
|
export { RegisterHandlers, EndpointMiddleware } from './object-handlers';
|
|
4
|
+
export { File as UploadedFile } from './router';
|
|
4
5
|
export { z as ZodSchema } from 'zod';
|
package/src/router.ts
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
InferDataFromUnifiedResponse,
|
|
8
8
|
} from './definition';
|
|
9
9
|
|
|
10
|
+
// Define the file type based on Express.Multer namespace
|
|
11
|
+
export type File = Express.Multer.File;
|
|
12
|
+
|
|
10
13
|
// Typed Request for Express handlers, now generic over TDef
|
|
11
14
|
export type TypedRequest<
|
|
12
15
|
TDef extends ApiDefinitionSchema,
|
|
@@ -17,7 +20,11 @@ export type TypedRequest<
|
|
|
17
20
|
ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>,
|
|
18
21
|
Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>,
|
|
19
22
|
L extends Record<string, any> = Record<string, any>
|
|
20
|
-
> = express.Request<P, any, ReqBody, Q, L>
|
|
23
|
+
> = express.Request<P, any, ReqBody, Q, L> & {
|
|
24
|
+
// Add file upload support
|
|
25
|
+
file?: File;
|
|
26
|
+
files?: File[] | { [fieldname: string]: File[] };
|
|
27
|
+
}
|
|
21
28
|
|
|
22
29
|
// --- Enhanced TypedResponse with res.respond, now generic over TDef ---
|
|
23
30
|
|
package/dist/apiClient.d.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { type ApiDefinitionSchema as BaseApiDefinitionSchema, // Renamed for clarity
|
|
2
|
-
type ApiRouteKey, type ApiClientParams, type ApiClientQuery, type ApiClientBody, type RouteSchema, type UnifiedError, type InferDataFromUnifiedResponse } from "./router/definition";
|
|
3
|
-
import { type ZodTypeAny } from 'zod';
|
|
4
|
-
/**
|
|
5
|
-
* Options for an HTTP request made by an adapter.
|
|
6
|
-
*/
|
|
7
|
-
export interface HttpRequestOptions {
|
|
8
|
-
method: RouteSchema['method'];
|
|
9
|
-
headers?: Record<string, string>;
|
|
10
|
-
body?: string | FormData;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Represents an HTTP response from an adapter.
|
|
14
|
-
* @template T The expected type of the JSON body.
|
|
15
|
-
*/
|
|
16
|
-
export interface HttpResponse<T = any> {
|
|
17
|
-
status: number;
|
|
18
|
-
headers: Headers;
|
|
19
|
-
json(): Promise<T>;
|
|
20
|
-
text(): Promise<string>;
|
|
21
|
-
/**
|
|
22
|
-
* Gets the underlying raw response object from the adapter (e.g., Fetch API's Response object).
|
|
23
|
-
* The type of this object depends on the adapter implementation.
|
|
24
|
-
*/
|
|
25
|
-
getRawResponse(): any;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Interface for an HTTP client adapter.
|
|
29
|
-
* Allows swapping out the underlying HTTP request mechanism (e.g., fetch, axios).
|
|
30
|
-
*/
|
|
31
|
-
export interface HttpClientAdapter {
|
|
32
|
-
/**
|
|
33
|
-
* Makes an HTTP request.
|
|
34
|
-
* @template T The expected type of the JSON response body.
|
|
35
|
-
* @param url The URL to request.
|
|
36
|
-
* @param options The request options.
|
|
37
|
-
* @returns A promise that resolves to an HttpResponse.
|
|
38
|
-
*/
|
|
39
|
-
request<T = any>(url: string, options: HttpRequestOptions): Promise<HttpResponse<T>>;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* An HttpClientAdapter implementation that uses the native Fetch API.
|
|
43
|
-
*/
|
|
44
|
-
export declare class FetchHttpClientAdapter implements HttpClientAdapter {
|
|
45
|
-
request<T = any>(url: string, options: HttpRequestOptions): Promise<HttpResponse<T>>;
|
|
46
|
-
}
|
|
47
|
-
type GetRoute<TActualDef extends BaseApiDefinitionSchema, TDomain extends keyof TActualDef, K extends ApiRouteKey<TActualDef, TDomain>> = TActualDef[TDomain][K] extends infer Rte ? Rte extends RouteSchema ? Rte : never : never;
|
|
48
|
-
type GetResponses<Rte extends RouteSchema> = Rte['responses'];
|
|
49
|
-
/**
|
|
50
|
-
* Discriminated union type for the result of callApi.
|
|
51
|
-
* @template TDef The specific ApiDefinition structure being used.
|
|
52
|
-
* @template TDomain The domain (controller) of the API.
|
|
53
|
-
* @template TRouteKey The key of the route within the domain.
|
|
54
|
-
*/
|
|
55
|
-
type ApiCallResultPayload<S_STATUS_NUM extends number, ActualSchema extends ZodTypeAny> = S_STATUS_NUM extends 422 ? {
|
|
56
|
-
status: S_STATUS_NUM;
|
|
57
|
-
error: UnifiedError;
|
|
58
|
-
rawResponse: any;
|
|
59
|
-
data?: undefined;
|
|
60
|
-
} : S_STATUS_NUM extends 204 ? {
|
|
61
|
-
status: S_STATUS_NUM;
|
|
62
|
-
data: void;
|
|
63
|
-
rawResponse: any;
|
|
64
|
-
error?: undefined;
|
|
65
|
-
} : {
|
|
66
|
-
status: S_STATUS_NUM;
|
|
67
|
-
data: InferDataFromUnifiedResponse<ActualSchema>;
|
|
68
|
-
rawResponse: any;
|
|
69
|
-
error?: undefined;
|
|
70
|
-
};
|
|
71
|
-
export type ApiCallResult<TActualDef extends BaseApiDefinitionSchema, TDomain extends keyof TActualDef, TRouteKey extends ApiRouteKey<TActualDef, TDomain>, CurrentRoute extends RouteSchema = GetRoute<TActualDef, TDomain, TRouteKey>, ResponsesMap = GetResponses<CurrentRoute>> = ResponsesMap extends Record<any, ZodTypeAny> ? {
|
|
72
|
-
[S_STATUS_NUM in keyof ResponsesMap]: S_STATUS_NUM extends number ? ResponsesMap[S_STATUS_NUM] extends infer ActualSchema extends ZodTypeAny ? ApiCallResultPayload<S_STATUS_NUM, ActualSchema> : never : never;
|
|
73
|
-
}[keyof ResponsesMap] : never;
|
|
74
|
-
/**
|
|
75
|
-
* Options for the callApi method.
|
|
76
|
-
* @template TDef The specific ApiDefinition structure being used.
|
|
77
|
-
* @template TDomainParam The domain (controller) of the API.
|
|
78
|
-
* @template TRouteKeyParam The key of the route within the domain.
|
|
79
|
-
*/
|
|
80
|
-
export type CallApiOptions<TActualDef extends BaseApiDefinitionSchema, // Made generic over TActualDef
|
|
81
|
-
TDomainParam extends keyof TActualDef, TRouteKeyParam extends ApiRouteKey<TActualDef, TDomainParam>> = {
|
|
82
|
-
params?: ApiClientParams<TActualDef, TDomainParam, TRouteKeyParam>;
|
|
83
|
-
query?: ApiClientQuery<TActualDef, TDomainParam, TRouteKeyParam>;
|
|
84
|
-
body?: ApiClientBody<TActualDef, TDomainParam, TRouteKeyParam>;
|
|
85
|
-
headers?: Record<string, string>;
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* A client for making API calls defined by an ApiDefinition.
|
|
89
|
-
* It uses an HttpClientAdapter for making actual HTTP requests and supports persistent headers.
|
|
90
|
-
*/
|
|
91
|
-
export declare class ApiClient<TActualDef extends BaseApiDefinitionSchema> {
|
|
92
|
-
private baseUrl;
|
|
93
|
-
private apiDefinitionObject;
|
|
94
|
-
private adapter;
|
|
95
|
-
private persistentHeaders;
|
|
96
|
-
/**
|
|
97
|
-
* Creates an instance of ApiClient.
|
|
98
|
-
* @param baseUrl The base URL for all API calls (e.g., 'http://localhost:3001').
|
|
99
|
-
* @param apiDefinitionObject The API definition object.
|
|
100
|
-
* @param adapter An instance of HttpClientAdapter to use for requests.
|
|
101
|
-
*/
|
|
102
|
-
constructor(baseUrl: string, apiDefinitionObject: TActualDef, // Parameter uses TActualDef
|
|
103
|
-
adapter: HttpClientAdapter);
|
|
104
|
-
/**
|
|
105
|
-
* Sets a persistent header that will be included in all subsequent API calls.
|
|
106
|
-
* If the header already exists, its value will be updated.
|
|
107
|
-
* @param name The name of the header.
|
|
108
|
-
* @param value The value of the header.
|
|
109
|
-
*/
|
|
110
|
-
setHeader(name: string, value: string): void;
|
|
111
|
-
/**
|
|
112
|
-
* Gets the value of a persistent header.
|
|
113
|
-
* @param name The name of the header.
|
|
114
|
-
* @returns The value of the header, or undefined if not set.
|
|
115
|
-
*/
|
|
116
|
-
getHeader(name: string): string | undefined;
|
|
117
|
-
/**
|
|
118
|
-
* Removes a persistent header.
|
|
119
|
-
* @param name The name of the header to remove.
|
|
120
|
-
*/
|
|
121
|
-
removeHeader(name: string): void;
|
|
122
|
-
/**
|
|
123
|
-
* Clears all persistent headers.
|
|
124
|
-
*/
|
|
125
|
-
clearHeaders(): void;
|
|
126
|
-
/**
|
|
127
|
-
* Makes an API call to a specified domain and route.
|
|
128
|
-
* @template TDomain The domain (controller) of the API.
|
|
129
|
-
* @template TRouteKey The key of the route within the domain.
|
|
130
|
-
* @template TInferredHandlers A type inferred from the handlers object, ensuring all defined statuses are handled.
|
|
131
|
-
* @param domain The API domain (e.g., 'user').
|
|
132
|
-
* @param routeKey The API route key (e.g., 'getUsers').
|
|
133
|
-
* @param callData Optional parameters, query, body, and headers for the request.
|
|
134
|
-
* @param handlers An object where keys are status codes and values are handler functions for those statuses.
|
|
135
|
-
* @returns A promise that resolves to the return value of the executed handler.
|
|
136
|
-
* @throws Error if the route configuration is invalid, a network error occurs, an unhandled status code is received, or JSON parsing fails.
|
|
137
|
-
*/
|
|
138
|
-
callApi<TDomain extends keyof TActualDef, TRouteKey extends ApiRouteKey<TActualDef, TDomain>, TInferredHandlers extends {
|
|
139
|
-
[KStatus in ApiCallResult<TActualDef, TDomain, TRouteKey>['status']]: (payload: Extract<ApiCallResult<TActualDef, TDomain, TRouteKey>, {
|
|
140
|
-
status: KStatus;
|
|
141
|
-
}>) => any;
|
|
142
|
-
}>(domain: TDomain, routeKey: TRouteKey, callData: CallApiOptions<TActualDef, TDomain, TRouteKey> | undefined, // Uses TActualDef
|
|
143
|
-
handlers: TInferredHandlers): Promise<{
|
|
144
|
-
[SKey in keyof TInferredHandlers]: TInferredHandlers[SKey] extends (...args: any[]) => infer R ? R : never;
|
|
145
|
-
}[keyof TInferredHandlers]>;
|
|
146
|
-
}
|
|
147
|
-
export {};
|
package/dist/apiClient.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ApiClient = exports.FetchHttpClientAdapter = void 0;
|
|
4
|
-
// --- Fetch Implementation of the Adapter ---
|
|
5
|
-
/**
|
|
6
|
-
* An HttpClientAdapter implementation that uses the native Fetch API.
|
|
7
|
-
*/
|
|
8
|
-
class FetchHttpClientAdapter {
|
|
9
|
-
async request(url, options) {
|
|
10
|
-
const fetchOptions = {
|
|
11
|
-
method: options.method,
|
|
12
|
-
headers: options.headers,
|
|
13
|
-
// Note: `credentials` (e.g., 'include' for cookies) is not set by default.
|
|
14
|
-
// It can be configured by extending this adapter or by managing cookies via the 'Cookie' header.
|
|
15
|
-
};
|
|
16
|
-
if (options.body !== undefined) {
|
|
17
|
-
fetchOptions.body = options.body;
|
|
18
|
-
}
|
|
19
|
-
const nativeFetchResponse = await fetch(url, fetchOptions);
|
|
20
|
-
return {
|
|
21
|
-
status: nativeFetchResponse.status,
|
|
22
|
-
headers: nativeFetchResponse.headers,
|
|
23
|
-
json: () => nativeFetchResponse.json(),
|
|
24
|
-
text: () => nativeFetchResponse.text(),
|
|
25
|
-
getRawResponse: () => nativeFetchResponse,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
exports.FetchHttpClientAdapter = FetchHttpClientAdapter;
|
|
30
|
-
// --- API Client Class ---
|
|
31
|
-
/**
|
|
32
|
-
* A client for making API calls defined by an ApiDefinition.
|
|
33
|
-
* It uses an HttpClientAdapter for making actual HTTP requests and supports persistent headers.
|
|
34
|
-
*/
|
|
35
|
-
class ApiClient {
|
|
36
|
-
baseUrl;
|
|
37
|
-
apiDefinitionObject; // Uses generic type TActualDef
|
|
38
|
-
adapter;
|
|
39
|
-
persistentHeaders = {};
|
|
40
|
-
/**
|
|
41
|
-
* Creates an instance of ApiClient.
|
|
42
|
-
* @param baseUrl The base URL for all API calls (e.g., 'http://localhost:3001').
|
|
43
|
-
* @param apiDefinitionObject The API definition object.
|
|
44
|
-
* @param adapter An instance of HttpClientAdapter to use for requests.
|
|
45
|
-
*/
|
|
46
|
-
constructor(baseUrl, apiDefinitionObject, // Parameter uses TActualDef
|
|
47
|
-
adapter) {
|
|
48
|
-
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
49
|
-
this.apiDefinitionObject = apiDefinitionObject;
|
|
50
|
-
this.adapter = adapter;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Sets a persistent header that will be included in all subsequent API calls.
|
|
54
|
-
* If the header already exists, its value will be updated.
|
|
55
|
-
* @param name The name of the header.
|
|
56
|
-
* @param value The value of the header.
|
|
57
|
-
*/
|
|
58
|
-
setHeader(name, value) {
|
|
59
|
-
this.persistentHeaders[name] = value;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Gets the value of a persistent header.
|
|
63
|
-
* @param name The name of the header.
|
|
64
|
-
* @returns The value of the header, or undefined if not set.
|
|
65
|
-
*/
|
|
66
|
-
getHeader(name) {
|
|
67
|
-
return this.persistentHeaders[name];
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Removes a persistent header.
|
|
71
|
-
* @param name The name of the header to remove.
|
|
72
|
-
*/
|
|
73
|
-
removeHeader(name) {
|
|
74
|
-
delete this.persistentHeaders[name];
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Clears all persistent headers.
|
|
78
|
-
*/
|
|
79
|
-
clearHeaders() {
|
|
80
|
-
this.persistentHeaders = {};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Makes an API call to a specified domain and route.
|
|
84
|
-
* @template TDomain The domain (controller) of the API.
|
|
85
|
-
* @template TRouteKey The key of the route within the domain.
|
|
86
|
-
* @template TInferredHandlers A type inferred from the handlers object, ensuring all defined statuses are handled.
|
|
87
|
-
* @param domain The API domain (e.g., 'user').
|
|
88
|
-
* @param routeKey The API route key (e.g., 'getUsers').
|
|
89
|
-
* @param callData Optional parameters, query, body, and headers for the request.
|
|
90
|
-
* @param handlers An object where keys are status codes and values are handler functions for those statuses.
|
|
91
|
-
* @returns A promise that resolves to the return value of the executed handler.
|
|
92
|
-
* @throws Error if the route configuration is invalid, a network error occurs, an unhandled status code is received, or JSON parsing fails.
|
|
93
|
-
*/
|
|
94
|
-
async callApi(domain, routeKey, callData, // Uses TActualDef
|
|
95
|
-
handlers) {
|
|
96
|
-
const routeInfo = this.apiDefinitionObject[domain][routeKey]; // Accessing from TActualDef instance
|
|
97
|
-
if (!routeInfo || typeof routeInfo.path !== 'string') {
|
|
98
|
-
throw new Error(`API route configuration ${String(domain)}.${String(routeKey)} not found or invalid.`);
|
|
99
|
-
}
|
|
100
|
-
let urlPath = routeInfo.path;
|
|
101
|
-
if (callData?.params) {
|
|
102
|
-
const params = callData.params;
|
|
103
|
-
for (const key in params) {
|
|
104
|
-
if (Object.prototype.hasOwnProperty.call(params, key) && params[key] !== undefined) {
|
|
105
|
-
urlPath = urlPath.replace(`:${key}`, String(params[key]));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const url = new URL(this.baseUrl + urlPath);
|
|
110
|
-
if (callData?.query) {
|
|
111
|
-
const queryParams = callData.query;
|
|
112
|
-
for (const key in queryParams) {
|
|
113
|
-
if (Object.prototype.hasOwnProperty.call(queryParams, key) && queryParams[key] !== undefined) {
|
|
114
|
-
url.searchParams.append(key, String(queryParams[key]));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const requestHeaders = {
|
|
119
|
-
...this.persistentHeaders,
|
|
120
|
-
'Content-Type': 'application/json',
|
|
121
|
-
...(callData?.headers || {}),
|
|
122
|
-
};
|
|
123
|
-
const adapterRequestOptions = {
|
|
124
|
-
method: routeInfo.method,
|
|
125
|
-
headers: requestHeaders,
|
|
126
|
-
};
|
|
127
|
-
if (routeInfo.method !== 'GET' && routeInfo.method !== 'HEAD' && callData?.body !== undefined) {
|
|
128
|
-
adapterRequestOptions.body = JSON.stringify(callData.body);
|
|
129
|
-
}
|
|
130
|
-
let adapterResponse;
|
|
131
|
-
try {
|
|
132
|
-
adapterResponse = await this.adapter.request(url.toString(), adapterRequestOptions);
|
|
133
|
-
}
|
|
134
|
-
catch (networkError) {
|
|
135
|
-
const errorMessage = networkError instanceof Error ? networkError.message : `Unknown network error calling API ${String(domain)}.${String(routeKey)}`;
|
|
136
|
-
console.error(`Network error for ${String(domain)}.${String(routeKey)}:`, networkError);
|
|
137
|
-
throw new Error(`Network error: ${errorMessage}`);
|
|
138
|
-
}
|
|
139
|
-
const runtimeStatus = adapterResponse.status;
|
|
140
|
-
const definedStatusCodes = Object.keys(routeInfo.responses).map(Number);
|
|
141
|
-
if (!definedStatusCodes.includes(runtimeStatus)) {
|
|
142
|
-
const responseText = await adapterResponse.text().catch(() => "Could not read response text.");
|
|
143
|
-
const errorMsg = `API ${String(domain)}.${String(routeKey)}: Received unhandled status code ${runtimeStatus}. Expected one of: ${definedStatusCodes.join(', ')}. Response: ${responseText}`;
|
|
144
|
-
console.error(errorMsg);
|
|
145
|
-
throw new Error(errorMsg);
|
|
146
|
-
}
|
|
147
|
-
const currentStatusLiteral = runtimeStatus;
|
|
148
|
-
// apiResultPayload now uses ApiCallResult with TActualDef
|
|
149
|
-
let apiResultPayload;
|
|
150
|
-
if (currentStatusLiteral === 204) {
|
|
151
|
-
apiResultPayload = {
|
|
152
|
-
status: 204,
|
|
153
|
-
data: undefined,
|
|
154
|
-
rawResponse: adapterResponse.getRawResponse(),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
let responseBodyJson;
|
|
159
|
-
const contentType = adapterResponse.headers.get("content-type");
|
|
160
|
-
if (contentType && contentType.includes("application/json")) {
|
|
161
|
-
try {
|
|
162
|
-
responseBodyJson = await adapterResponse.json();
|
|
163
|
-
}
|
|
164
|
-
catch (e) {
|
|
165
|
-
const parseErrorMsg = `API ${String(domain)}.${String(routeKey)}: Failed to parse JSON for status ${currentStatusLiteral}. Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
166
|
-
console.error(parseErrorMsg, adapterResponse.getRawResponse());
|
|
167
|
-
throw new Error(parseErrorMsg);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else if (runtimeStatus >= 400) { // Handle non-JSON error responses
|
|
171
|
-
const responseText = await adapterResponse.text().catch(() => "Could not read response text.");
|
|
172
|
-
if (currentStatusLiteral === 422) { // Try to conform to UnifiedError for 422
|
|
173
|
-
responseBodyJson = {
|
|
174
|
-
error: [{ field: 'general', type: 'general', message: `Non-JSON error response for 422: ${responseText}` }] // Adjusted: type to 'general'
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
else { // For other non-JSON errors, data will likely be undefined. Log a warning.
|
|
178
|
-
console.warn(`API ${String(domain)}.${String(routeKey)}: Received non-JSON response for status ${currentStatusLiteral}. Response: ${responseText}`);
|
|
179
|
-
// responseBodyJson remains undefined or as is, data extraction below will handle it.
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (currentStatusLiteral === 422) {
|
|
183
|
-
const errorData = responseBodyJson?.error || [{ field: 'general', type: 'general', message: `HTTP error 422: ${await adapterResponse.text().catch(() => 'Unknown error text')}` }];
|
|
184
|
-
// Assign directly to apiResultPayload, relying on its type ApiCallResult<TActualDef, TDomain, TRouteKey>
|
|
185
|
-
// to correctly match the 422 variant.
|
|
186
|
-
apiResultPayload = {
|
|
187
|
-
status: 422,
|
|
188
|
-
error: errorData,
|
|
189
|
-
rawResponse: adapterResponse.getRawResponse(),
|
|
190
|
-
}; // Force cast via unknown
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
// Assuming responseBodyJson is an object like { data: <actual_payload> }
|
|
194
|
-
// as per backend contract for non-204/non-422 responses.
|
|
195
|
-
apiResultPayload = {
|
|
196
|
-
status: currentStatusLiteral,
|
|
197
|
-
data: responseBodyJson.data,
|
|
198
|
-
rawResponse: adapterResponse.getRawResponse(),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const handler = handlers[apiResultPayload.status];
|
|
203
|
-
return handler(apiResultPayload); // Reverting to `as any` as TS struggles with direct narrowing here
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
exports.ApiClient = ApiClient;
|