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/README.md
CHANGED
|
@@ -27,7 +27,7 @@ This module is specifically designed to make coding with Large Language Models (
|
|
|
27
27
|
## 📦 Installation
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
npm install ts-typed-api
|
|
30
|
+
npm install --save ts-typed-api
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
|
|
@@ -144,6 +144,63 @@ async function runClientExample(): Promise<void> {
|
|
|
144
144
|
|
|
145
145
|
**Now both server and client are type safe and in sync! The moment you change the definition of the API, type system will let you know about potential changes you need to handle (like additional response code or a change request body schema).**
|
|
146
146
|
|
|
147
|
+
### 4. File Upload Example
|
|
148
|
+
|
|
149
|
+
Handle file uploads with type-safe validation:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Define file upload endpoints
|
|
153
|
+
const FileUploadApiDefinition = CreateApiDefinition({
|
|
154
|
+
prefix: '/api',
|
|
155
|
+
endpoints: {
|
|
156
|
+
files: {
|
|
157
|
+
uploadSingle: {
|
|
158
|
+
path: '/upload/single',
|
|
159
|
+
method: 'POST',
|
|
160
|
+
body: z.object({
|
|
161
|
+
description: z.string().optional(),
|
|
162
|
+
}),
|
|
163
|
+
fileUpload: {
|
|
164
|
+
single: {
|
|
165
|
+
fieldName: 'file',
|
|
166
|
+
maxSize: 5 * 1024 * 1024, // 5MB
|
|
167
|
+
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif']
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
responses: CreateResponses({
|
|
171
|
+
200: z.object({
|
|
172
|
+
message: z.string(),
|
|
173
|
+
fileInfo: z.object({
|
|
174
|
+
originalName: z.string(),
|
|
175
|
+
size: z.number(),
|
|
176
|
+
mimetype: z.string()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Implement handler
|
|
186
|
+
RegisterHandlers(app, FileUploadApiDefinition, {
|
|
187
|
+
files: {
|
|
188
|
+
uploadSingle: async (req, res) => {
|
|
189
|
+
const file = req.file as UploadedFile | undefined;
|
|
190
|
+
|
|
191
|
+
res.respond(200, {
|
|
192
|
+
message: 'File uploaded successfully',
|
|
193
|
+
fileInfo: {
|
|
194
|
+
originalName: file!.originalname,
|
|
195
|
+
size: file!.size,
|
|
196
|
+
mimetype: file!.mimetype
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
147
204
|
## 🌟 Features
|
|
148
205
|
|
|
149
206
|
### Custom HTTP Client Adapters
|
package/dist/definition.d.ts
CHANGED
|
@@ -73,12 +73,36 @@ type CreateResponsesReturnType<InputSchemas extends Partial<Record<AllowedInputS
|
|
|
73
73
|
422: typeof errorUnifiedResponseSchema;
|
|
74
74
|
};
|
|
75
75
|
export declare function CreateResponses<TInputMap extends Partial<Record<AllowedInputStatusCode, InputSchemaOrMarker>>>(schemas: TInputMap): CreateResponsesReturnType<TInputMap>;
|
|
76
|
+
export interface FileUploadConfig {
|
|
77
|
+
single?: {
|
|
78
|
+
fieldName: string;
|
|
79
|
+
maxSize?: number;
|
|
80
|
+
allowedMimeTypes?: string[];
|
|
81
|
+
};
|
|
82
|
+
array?: {
|
|
83
|
+
fieldName: string;
|
|
84
|
+
maxCount?: number;
|
|
85
|
+
maxSize?: number;
|
|
86
|
+
allowedMimeTypes?: string[];
|
|
87
|
+
};
|
|
88
|
+
fields?: Array<{
|
|
89
|
+
fieldName: string;
|
|
90
|
+
maxCount?: number;
|
|
91
|
+
maxSize?: number;
|
|
92
|
+
allowedMimeTypes?: string[];
|
|
93
|
+
}>;
|
|
94
|
+
any?: {
|
|
95
|
+
maxSize?: number;
|
|
96
|
+
allowedMimeTypes?: string[];
|
|
97
|
+
};
|
|
98
|
+
}
|
|
76
99
|
export interface RouteSchema {
|
|
77
100
|
path: string;
|
|
78
101
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD';
|
|
79
102
|
params?: ZodTypeAny;
|
|
80
103
|
query?: ZodTypeAny;
|
|
81
104
|
body?: ZodTypeAny;
|
|
105
|
+
fileUpload?: FileUploadConfig;
|
|
82
106
|
responses: Record<number, ZodTypeAny>;
|
|
83
107
|
}
|
|
84
108
|
export type ApiDefinitionSchema = {
|
|
@@ -118,4 +142,50 @@ export type ApiClientParams<TDef extends ApiDefinitionSchema, TDomain extends ke
|
|
|
118
142
|
export type ApiClientQuery<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends ApiRouteKey<TDef, TDomain>> = TDef['endpoints'][TDomain][TRouteName] extends {
|
|
119
143
|
query: infer Q extends ZodTypeAny;
|
|
120
144
|
} ? z.input<Q> : undefined;
|
|
145
|
+
export declare const fileSchema: z.ZodObject<{
|
|
146
|
+
fieldname: z.ZodString;
|
|
147
|
+
originalname: z.ZodString;
|
|
148
|
+
encoding: z.ZodString;
|
|
149
|
+
mimetype: z.ZodString;
|
|
150
|
+
size: z.ZodNumber;
|
|
151
|
+
buffer: z.ZodType<Buffer<ArrayBufferLike>, z.ZodTypeDef, Buffer<ArrayBufferLike>>;
|
|
152
|
+
destination: z.ZodOptional<z.ZodString>;
|
|
153
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
154
|
+
path: z.ZodOptional<z.ZodString>;
|
|
155
|
+
stream: z.ZodOptional<z.ZodAny>;
|
|
156
|
+
}, "strip", z.ZodTypeAny, {
|
|
157
|
+
fieldname: string;
|
|
158
|
+
originalname: string;
|
|
159
|
+
encoding: string;
|
|
160
|
+
mimetype: string;
|
|
161
|
+
size: number;
|
|
162
|
+
buffer: Buffer<ArrayBufferLike>;
|
|
163
|
+
path?: string | undefined;
|
|
164
|
+
destination?: string | undefined;
|
|
165
|
+
filename?: string | undefined;
|
|
166
|
+
stream?: any;
|
|
167
|
+
}, {
|
|
168
|
+
fieldname: string;
|
|
169
|
+
originalname: string;
|
|
170
|
+
encoding: string;
|
|
171
|
+
mimetype: string;
|
|
172
|
+
size: number;
|
|
173
|
+
buffer: Buffer<ArrayBufferLike>;
|
|
174
|
+
path?: string | undefined;
|
|
175
|
+
destination?: string | undefined;
|
|
176
|
+
filename?: string | undefined;
|
|
177
|
+
stream?: any;
|
|
178
|
+
}>;
|
|
179
|
+
export type FileType = z.infer<typeof fileSchema>;
|
|
180
|
+
export declare function createFileValidationSchema(options?: {
|
|
181
|
+
maxSize?: number;
|
|
182
|
+
allowedMimeTypes?: string[];
|
|
183
|
+
required?: boolean;
|
|
184
|
+
}): z.ZodTypeAny;
|
|
185
|
+
export declare function createFilesArrayValidationSchema(options?: {
|
|
186
|
+
maxCount?: number;
|
|
187
|
+
maxSize?: number;
|
|
188
|
+
allowedMimeTypes?: string[];
|
|
189
|
+
required?: boolean;
|
|
190
|
+
}): z.ZodArray<z.ZodTypeAny, "many"> | z.ZodOptional<z.ZodArray<z.ZodTypeAny, "many">>;
|
|
121
191
|
export {};
|
package/dist/definition.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HttpServerErrorCodes = exports.HttpClientErrorCodes = exports.HttpSuccessCodes = exports.TsTypeMarker = void 0;
|
|
3
|
+
exports.fileSchema = exports.HttpServerErrorCodes = exports.HttpClientErrorCodes = exports.HttpSuccessCodes = exports.TsTypeMarker = void 0;
|
|
4
4
|
exports.CustomResponse = CustomResponse;
|
|
5
5
|
exports.CreateResponses = CreateResponses;
|
|
6
6
|
exports.CreateApiDefinition = CreateApiDefinition;
|
|
7
|
+
exports.createFileValidationSchema = createFileValidationSchema;
|
|
8
|
+
exports.createFilesArrayValidationSchema = createFilesArrayValidationSchema;
|
|
7
9
|
const zod_1 = require("zod");
|
|
8
10
|
// Marker class for raw TypeScript types
|
|
9
11
|
class TsTypeMarker {
|
|
@@ -61,6 +63,9 @@ function CreateResponses(schemas) {
|
|
|
61
63
|
else if (schemaOrMarker instanceof zod_1.ZodType) { // It's a Zod schema
|
|
62
64
|
builtResult[numericKey] = createSuccessUnifiedResponseSchema(schemaOrMarker);
|
|
63
65
|
}
|
|
66
|
+
else {
|
|
67
|
+
builtResult[numericKey] = createSuccessUnifiedResponseSchema(schemaOrMarker);
|
|
68
|
+
}
|
|
64
69
|
// Note: If schemaOrMarker is something else, it would be a type error
|
|
65
70
|
// based on InputSchemaOrMarker, or this runtime check would skip it.
|
|
66
71
|
}
|
|
@@ -77,3 +82,41 @@ function CreateResponses(schemas) {
|
|
|
77
82
|
function CreateApiDefinition(definition) {
|
|
78
83
|
return definition;
|
|
79
84
|
}
|
|
85
|
+
// --- File Upload Validation Schemas ---
|
|
86
|
+
// Schema for validating uploaded files
|
|
87
|
+
exports.fileSchema = zod_1.z.object({
|
|
88
|
+
fieldname: zod_1.z.string(),
|
|
89
|
+
originalname: zod_1.z.string(),
|
|
90
|
+
encoding: zod_1.z.string(),
|
|
91
|
+
mimetype: zod_1.z.string(),
|
|
92
|
+
size: zod_1.z.number(),
|
|
93
|
+
buffer: zod_1.z.instanceof(Buffer),
|
|
94
|
+
destination: zod_1.z.string().optional(),
|
|
95
|
+
filename: zod_1.z.string().optional(),
|
|
96
|
+
path: zod_1.z.string().optional(),
|
|
97
|
+
stream: zod_1.z.any().optional(),
|
|
98
|
+
});
|
|
99
|
+
// Helper function to create file validation schema with constraints
|
|
100
|
+
function createFileValidationSchema(options) {
|
|
101
|
+
let schema = exports.fileSchema;
|
|
102
|
+
if (options?.maxSize) {
|
|
103
|
+
schema = schema.refine((file) => file.size <= options.maxSize, { message: `File size must be less than ${options.maxSize} bytes` });
|
|
104
|
+
}
|
|
105
|
+
if (options?.allowedMimeTypes && options.allowedMimeTypes.length > 0) {
|
|
106
|
+
schema = schema.refine((file) => options.allowedMimeTypes.includes(file.mimetype), { message: `File type must be one of: ${options.allowedMimeTypes.join(', ')}` });
|
|
107
|
+
}
|
|
108
|
+
return options?.required === false ? schema.optional() : schema;
|
|
109
|
+
}
|
|
110
|
+
// Helper function to create array of files validation schema
|
|
111
|
+
function createFilesArrayValidationSchema(options) {
|
|
112
|
+
const singleFileSchema = createFileValidationSchema({
|
|
113
|
+
maxSize: options?.maxSize,
|
|
114
|
+
allowedMimeTypes: options?.allowedMimeTypes,
|
|
115
|
+
required: true, // Individual files in array are required
|
|
116
|
+
});
|
|
117
|
+
let schema = zod_1.z.array(singleFileSchema);
|
|
118
|
+
if (options?.maxCount) {
|
|
119
|
+
schema = schema.max(options.maxCount, `Maximum ${options.maxCount} files allowed`);
|
|
120
|
+
}
|
|
121
|
+
return options?.required === false ? schema.optional() : schema;
|
|
122
|
+
}
|
package/dist/handler.js
CHANGED
|
@@ -1,7 +1,155 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.registerRouteHandlers = registerRouteHandlers;
|
|
4
7
|
const zod_1 = require("zod");
|
|
8
|
+
const multer_1 = __importDefault(require("multer"));
|
|
9
|
+
// Helper function to create multer middleware based on file upload configuration
|
|
10
|
+
function createFileUploadMiddleware(config) {
|
|
11
|
+
// Default multer configuration
|
|
12
|
+
const storage = multer_1.default.memoryStorage(); // Store files in memory by default
|
|
13
|
+
let multerMiddleware;
|
|
14
|
+
if (config.single) {
|
|
15
|
+
const upload = (0, multer_1.default)({
|
|
16
|
+
storage,
|
|
17
|
+
limits: {
|
|
18
|
+
fileSize: config.single.maxSize || 10 * 1024 * 1024, // Default 10MB
|
|
19
|
+
},
|
|
20
|
+
fileFilter: (req, file, cb) => {
|
|
21
|
+
if (config.single.allowedMimeTypes && !config.single.allowedMimeTypes.includes(file.mimetype)) {
|
|
22
|
+
cb(new Error(`File type ${file.mimetype} not allowed`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
cb(null, true);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
multerMiddleware = upload.single(config.single.fieldName);
|
|
29
|
+
}
|
|
30
|
+
else if (config.array) {
|
|
31
|
+
const upload = (0, multer_1.default)({
|
|
32
|
+
storage,
|
|
33
|
+
limits: {
|
|
34
|
+
fileSize: config.array.maxSize || 10 * 1024 * 1024, // Default 10MB per file
|
|
35
|
+
files: config.array.maxCount || 10, // Default max 10 files
|
|
36
|
+
},
|
|
37
|
+
fileFilter: (req, file, cb) => {
|
|
38
|
+
if (config.array.allowedMimeTypes && !config.array.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.array(config.array.fieldName, config.array.maxCount);
|
|
46
|
+
}
|
|
47
|
+
else if (config.fields) {
|
|
48
|
+
const upload = (0, multer_1.default)({
|
|
49
|
+
storage,
|
|
50
|
+
limits: {
|
|
51
|
+
fileSize: Math.max(...config.fields.map(f => f.maxSize || 10 * 1024 * 1024)), // Use max size from all fields
|
|
52
|
+
},
|
|
53
|
+
fileFilter: (req, file, cb) => {
|
|
54
|
+
const fieldConfig = config.fields.find(f => f.fieldName === file.fieldname);
|
|
55
|
+
if (fieldConfig?.allowedMimeTypes && !fieldConfig.allowedMimeTypes.includes(file.mimetype)) {
|
|
56
|
+
cb(new Error(`File type ${file.mimetype} not allowed for field ${file.fieldname}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
cb(null, true);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const fields = config.fields.map(f => ({ name: f.fieldName, maxCount: f.maxCount || 1 }));
|
|
63
|
+
multerMiddleware = upload.fields(fields);
|
|
64
|
+
}
|
|
65
|
+
else if (config.any) {
|
|
66
|
+
const upload = (0, multer_1.default)({
|
|
67
|
+
storage,
|
|
68
|
+
limits: {
|
|
69
|
+
fileSize: config.any.maxSize || 10 * 1024 * 1024, // Default 10MB per file
|
|
70
|
+
},
|
|
71
|
+
fileFilter: (req, file, cb) => {
|
|
72
|
+
if (config.any.allowedMimeTypes && !config.any.allowedMimeTypes.includes(file.mimetype)) {
|
|
73
|
+
cb(new Error(`File type ${file.mimetype} not allowed`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
cb(null, true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
multerMiddleware = upload.any();
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Fallback - should not reach here if config is valid
|
|
83
|
+
throw new Error('Invalid file upload configuration');
|
|
84
|
+
}
|
|
85
|
+
// Wrap multer middleware with error handling to format errors as 422 JSON responses
|
|
86
|
+
return (req, res, next) => {
|
|
87
|
+
multerMiddleware(req, res, (error) => {
|
|
88
|
+
if (error) {
|
|
89
|
+
// Convert multer errors to UnifiedError format
|
|
90
|
+
const mappedErrors = [];
|
|
91
|
+
if (error instanceof multer_1.default.MulterError) {
|
|
92
|
+
let errorMessage = error.message;
|
|
93
|
+
let fieldName = 'file';
|
|
94
|
+
switch (error.code) {
|
|
95
|
+
case 'LIMIT_FILE_SIZE':
|
|
96
|
+
errorMessage = 'File size exceeds the allowed limit';
|
|
97
|
+
break;
|
|
98
|
+
case 'LIMIT_FILE_COUNT':
|
|
99
|
+
errorMessage = 'Too many files uploaded';
|
|
100
|
+
break;
|
|
101
|
+
case 'LIMIT_UNEXPECTED_FILE':
|
|
102
|
+
errorMessage = `Unexpected field: ${error.field}`;
|
|
103
|
+
fieldName = error.field || 'file';
|
|
104
|
+
break;
|
|
105
|
+
case 'LIMIT_FIELD_KEY':
|
|
106
|
+
errorMessage = 'Field name too long';
|
|
107
|
+
break;
|
|
108
|
+
case 'LIMIT_FIELD_VALUE':
|
|
109
|
+
errorMessage = 'Field value too long';
|
|
110
|
+
break;
|
|
111
|
+
case 'LIMIT_FIELD_COUNT':
|
|
112
|
+
errorMessage = 'Too many fields';
|
|
113
|
+
break;
|
|
114
|
+
case 'LIMIT_PART_COUNT':
|
|
115
|
+
errorMessage = 'Too many parts';
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
errorMessage = error.message || 'File upload error';
|
|
119
|
+
}
|
|
120
|
+
mappedErrors.push({
|
|
121
|
+
field: fieldName,
|
|
122
|
+
message: errorMessage,
|
|
123
|
+
type: 'body',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else if (error instanceof Error) {
|
|
127
|
+
// Handle custom errors from fileFilter
|
|
128
|
+
mappedErrors.push({
|
|
129
|
+
field: 'file',
|
|
130
|
+
message: error.message,
|
|
131
|
+
type: 'body',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
mappedErrors.push({
|
|
136
|
+
field: 'file',
|
|
137
|
+
message: 'File upload error',
|
|
138
|
+
type: 'body',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// Send 422 response with structured error format
|
|
142
|
+
res.status(422).json({
|
|
143
|
+
data: null,
|
|
144
|
+
error: mappedErrors
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
next();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
}
|
|
5
153
|
// Register route handlers with Express, now generic over TDef
|
|
6
154
|
function registerRouteHandlers(app, apiDefinition, // Pass the actual API definition object
|
|
7
155
|
routeHandlers, // Use the generic handler type
|
|
@@ -150,6 +298,17 @@ middlewares) {
|
|
|
150
298
|
};
|
|
151
299
|
// Create middleware wrappers that include endpoint information
|
|
152
300
|
const middlewareWrappers = [];
|
|
301
|
+
// Add file upload middleware if configured
|
|
302
|
+
if (routeDefinition.fileUpload) {
|
|
303
|
+
try {
|
|
304
|
+
const fileUploadMiddleware = createFileUploadMiddleware(routeDefinition.fileUpload);
|
|
305
|
+
middlewareWrappers.push(fileUploadMiddleware);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.error(`Error creating file upload middleware for ${currentDomain}.${currentRouteKey}:`, error);
|
|
309
|
+
return; // Skip this route if file upload middleware creation fails
|
|
310
|
+
}
|
|
311
|
+
}
|
|
153
312
|
if (middlewares && middlewares.length > 0) {
|
|
154
313
|
middlewares.forEach(middleware => {
|
|
155
314
|
const wrappedMiddleware = async (req, res, next) => {
|
package/dist/index.d.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/dist/router.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { ApiDefinitionSchema, // Changed from ApiDefinition
|
|
3
3
|
ApiBody, ApiParams, ApiQuery, InferDataFromUnifiedResponse } from './definition';
|
|
4
|
+
export type File = Express.Multer.File;
|
|
4
5
|
export type TypedRequest<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain], // Using direct keyof for simplicity here
|
|
5
|
-
P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteKey>, ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>, Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>, L extends Record<string, any> = Record<string, any>> = express.Request<P, any, ReqBody, Q, L
|
|
6
|
+
P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteKey>, ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>, Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>, L extends Record<string, any> = Record<string, any>> = express.Request<P, any, ReqBody, Q, L> & {
|
|
7
|
+
file?: File;
|
|
8
|
+
files?: File[] | {
|
|
9
|
+
[fieldname: string]: File[];
|
|
10
|
+
};
|
|
11
|
+
};
|
|
6
12
|
type ResponseDataForStatus<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain], TStatus extends keyof TDef['endpoints'][TDomain][TRouteName]['responses'] & number> = InferDataFromUnifiedResponse<TDef['endpoints'][TDomain][TRouteName]['responses'][TStatus]>;
|
|
7
13
|
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;
|
|
8
14
|
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> {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
CreateApiDefinition,
|
|
5
|
+
CreateResponses,
|
|
6
|
+
RegisterHandlers,
|
|
7
|
+
UploadedFile
|
|
8
|
+
} from '../src';
|
|
9
|
+
|
|
10
|
+
// Define API with file upload endpoints
|
|
11
|
+
const FileUploadApiDefinition = CreateApiDefinition({
|
|
12
|
+
prefix: '/api',
|
|
13
|
+
endpoints: {
|
|
14
|
+
files: {
|
|
15
|
+
// Single file upload
|
|
16
|
+
uploadSingle: {
|
|
17
|
+
path: '/upload/single',
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: z.object({
|
|
20
|
+
description: z.string().optional(),
|
|
21
|
+
}),
|
|
22
|
+
fileUpload: {
|
|
23
|
+
single: {
|
|
24
|
+
fieldName: 'file',
|
|
25
|
+
maxSize: 5 * 1024 * 1024, // 5MB
|
|
26
|
+
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/gif']
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
responses: CreateResponses({
|
|
30
|
+
200: z.object({
|
|
31
|
+
message: z.string(),
|
|
32
|
+
fileInfo: z.object({
|
|
33
|
+
originalName: z.string(),
|
|
34
|
+
size: z.number(),
|
|
35
|
+
mimetype: z.string()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Multiple files upload (array)
|
|
42
|
+
uploadMultiple: {
|
|
43
|
+
path: '/upload/multiple',
|
|
44
|
+
method: 'POST',
|
|
45
|
+
body: z.object({
|
|
46
|
+
category: z.string(),
|
|
47
|
+
}),
|
|
48
|
+
fileUpload: {
|
|
49
|
+
array: {
|
|
50
|
+
fieldName: 'files',
|
|
51
|
+
maxCount: 5,
|
|
52
|
+
maxSize: 2 * 1024 * 1024, // 2MB per file
|
|
53
|
+
allowedMimeTypes: ['image/jpeg', 'image/png']
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
responses: CreateResponses({
|
|
57
|
+
200: z.object({
|
|
58
|
+
message: z.string(),
|
|
59
|
+
uploadedFiles: z.array(z.object({
|
|
60
|
+
originalName: z.string(),
|
|
61
|
+
size: z.number(),
|
|
62
|
+
mimetype: z.string()
|
|
63
|
+
}))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Create Express app
|
|
72
|
+
const app = express();
|
|
73
|
+
const port = 3002;
|
|
74
|
+
|
|
75
|
+
// Note: Don't use express.json() for file upload routes as it conflicts with multipart parsing
|
|
76
|
+
// Only use it for non-file routes or apply it selectively
|
|
77
|
+
|
|
78
|
+
// Register handlers
|
|
79
|
+
RegisterHandlers(app, FileUploadApiDefinition, {
|
|
80
|
+
files: {
|
|
81
|
+
uploadSingle: async (req, res) => {
|
|
82
|
+
// req.file contains the uploaded file
|
|
83
|
+
// req.body contains the form data
|
|
84
|
+
|
|
85
|
+
const file = req.file as UploadedFile | undefined;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
|
|
89
|
+
// Process the file here (save to disk, upload to cloud, etc.)
|
|
90
|
+
console.log('Uploaded file:', {
|
|
91
|
+
name: file!.originalname,
|
|
92
|
+
size: file!.size,
|
|
93
|
+
type: file!.mimetype
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
res.respond(200, {
|
|
97
|
+
message: 'File uploaded successfully',
|
|
98
|
+
fileInfo: {
|
|
99
|
+
originalName: file!.originalname,
|
|
100
|
+
size: file!.size,
|
|
101
|
+
mimetype: file!.mimetype
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('File validation error:', error);
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
uploadMultiple: async (req, res) => {
|
|
111
|
+
// req.files contains array of uploaded files
|
|
112
|
+
// req.body contains the form data
|
|
113
|
+
const files = req.files as UploadedFile[] | undefined;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Process the files here
|
|
117
|
+
const uploadedFiles = files!.map(file => ({
|
|
118
|
+
originalName: file.originalname,
|
|
119
|
+
size: file.size,
|
|
120
|
+
mimetype: file.mimetype
|
|
121
|
+
}));
|
|
122
|
+
console.log('Uploaded files:', uploadedFiles, req.body.category);
|
|
123
|
+
|
|
124
|
+
res.respond(200, {
|
|
125
|
+
message: `${uploadedFiles.length} files uploaded successfully`,
|
|
126
|
+
uploadedFiles
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('Files validation error:', error);
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
app.listen(port, () => {
|
|
138
|
+
console.log(`File upload server listening at http://localhost:${port}`);
|
|
139
|
+
console.log('\nExample curl commands:');
|
|
140
|
+
console.log('\n1. Single file upload:');
|
|
141
|
+
console.log(`curl -X POST http://localhost:${port}/api/upload/single \\`);
|
|
142
|
+
console.log(` -F "file=@/path/to/image.jpg" \\`);
|
|
143
|
+
console.log(` -F "description=My uploaded image"`);
|
|
144
|
+
|
|
145
|
+
console.log('\n2. Multiple files upload:');
|
|
146
|
+
console.log(`curl -X POST http://localhost:${port}/api/upload/multiple \\`);
|
|
147
|
+
console.log(` -F "files=@/path/to/image1.jpg" \\`);
|
|
148
|
+
console.log(` -F "files=@/path/to/image2.png" \\`);
|
|
149
|
+
console.log(` -F "category=photos"`);
|
|
150
|
+
|
|
151
|
+
console.log('\n3. Mixed fields upload:');
|
|
152
|
+
console.log(`curl -X POST http://localhost:${port}/api/upload/mixed \\`);
|
|
153
|
+
console.log(` -F "avatar=@/path/to/avatar.jpg" \\`);
|
|
154
|
+
console.log(` -F "documents=@/path/to/doc1.pdf" \\`);
|
|
155
|
+
console.log(` -F "documents=@/path/to/doc2.txt" \\`);
|
|
156
|
+
console.log(` -F "title=My Upload" \\`);
|
|
157
|
+
console.log(` -F "tags=tag1,tag2"`);
|
|
158
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-typed-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
"license": "ISC",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@types/express": "^5.0.3",
|
|
29
|
+
"@types/multer": "^1.4.13",
|
|
29
30
|
"@types/node": "^24.0.3",
|
|
30
31
|
"express": "^5.1.0",
|
|
32
|
+
"multer": "^2.0.1",
|
|
31
33
|
"zod": "^3.22.4"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|