Skip to content

Commit 010f072

Browse files
Harrison IfeanyichukwuHarrison Ifeanyichukwu
authored andcommitted
feat: merge request.files into request.data field, delete files and body fields
update deletes request.files and request.body fields BREAKING CHANGE: request.files and request.body fields no longer exists, use request.data instead
1 parent 39b76bf commit 010f072

File tree

6 files changed

+90
-96
lines changed

6 files changed

+90
-96
lines changed

server.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ const server = new Server();
66

77
// process form upload from our examples index.html file
88
server.post('/', function (req, res) {
9-
const result = JSON.stringify(Object.assign({}, req.body, req.files));
9+
const result = JSON.stringify(req.data, (key, value) => {
10+
if (Buffer.isBuffer(value)) {
11+
return undefined;
12+
}
13+
return value;
14+
});
1015

11-
writeFileSync('./upload.pdf', req.files['file-cv'].data);
16+
writeFileSync('./upload.pdf', req.data['file-cv'].data);
1217
return res.json(jsBeautify.js(result));
1318
});
1419

src/@types/index.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,12 @@ export interface FileEntry {
163163
type: string;
164164
}
165165

166-
export interface Files {
167-
[fieldName: string]: FileEntry | Array<FileEntry>;
166+
export interface Query {
167+
[propName: string]: string | string[];
168168
}
169169

170170
export interface Data {
171-
[propName: string]: string | string[];
171+
[propName: string]: string | string[] | FileEntry | FileEntry[];
172172
}
173173

174174
export type PathParameters<T extends string = string> = Record<
@@ -201,31 +201,43 @@ export interface RouteParameter {
201201

202202
export type Routes = Record<Exclude<Method, '*'>, RouteInstance[]>;
203203

204-
export interface RouteResponse<Data = {}, Errors = {}> {
204+
export interface RouteResponse<DataType = {}, ErrorsType = {}> {
205205
statusCode?: number;
206206
message?: string;
207-
data?: Data;
208-
errors?: Errors;
207+
data?: DataType;
208+
errors?: ErrorsType;
209209
headers?: IncomingHttpHeaders;
210210
}
211211

212-
export interface APIExecutor<RequestBody, Data, Errors = {}> {
213-
(arg: {
214-
/**
215-
* request body, should be a combination of parsed post body and url search params
216-
*/
217-
body: RequestBody;
212+
export interface ApiExecutorProps<
213+
RequestDataType,
214+
PathParametersType = Record<string, string | boolean | number>
215+
> {
216+
/**
217+
* request data type, a combination of query parameters and request body
218+
*/
219+
data: RequestDataType;
218220

219-
/**
220-
* request http headers
221-
*/
222-
headers: IncomingHttpHeaders;
221+
/**
222+
* request http headers
223+
*/
224+
headers: IncomingHttpHeaders;
223225

224-
/**
225-
* request path parameters, as contained in the routing path
226-
*/
227-
pathParams: PathParameters;
228-
}): Promise<RouteResponse<Data, Errors> | null>;
226+
/**
227+
* request path parameters, as contained in the routing path
228+
*/
229+
pathParams: PathParametersType;
230+
}
231+
232+
export interface APIExecutor<
233+
RequestDataType = {},
234+
PathParametersType = {},
235+
ResponseDataType = {},
236+
ResponseErrorsType = {}
237+
> {
238+
(
239+
arg: ApiExecutorProps<RequestDataType, PathParametersType>
240+
): Promise<RouteResponse<ResponseDataType, ResponseErrorsType> | null>;
229241

230242
/**
231243
* assigned name of the handler

src/modules/BodyParser.ts

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Data, Files, MultipartHeaders, FileEntry } from '../@types/index';
1+
import { Data, MultipartHeaders, FileEntry, Query } from '../@types/index';
22
import { generateRandomText } from '@teclone/utils';
33
import { CRLF, BLANK_LINE } from './Constants';
44

@@ -18,31 +18,23 @@ export class BodyParser {
1818
}
1919
}
2020

21-
/**
22-
* assigns a single field or multi value field value to the body
23-
*/
24-
private assignBodyValue(body: Data, fieldName: string, value: string) {
25-
const { name, isMultiValue } = this.resolveFieldName(fieldName);
26-
27-
let target: string | string[] = value;
28-
if (isMultiValue) {
29-
target = body[name] || [];
30-
(target as string[]).push(value);
31-
}
32-
body[name] = target;
33-
}
34-
3521
/**
3622
* stores a file into the given files object
3723
*/
38-
private assignFileValue(files: Files, fieldName: string, value: FileEntry) {
24+
private assignDataEntry(
25+
data: Data,
26+
fieldName: string,
27+
value: FileEntry | string
28+
) {
3929
const { name, isMultiValue } = this.resolveFieldName(fieldName);
40-
let target: FileEntry | FileEntry[] = value;
41-
if (isMultiValue) {
42-
target = files[name] || [];
43-
(target as FileEntry[]).push(value);
30+
if (!isMultiValue) {
31+
data[name] = value;
32+
return;
33+
}
34+
if (typeof data[name] === 'undefined') {
35+
data[name] = [];
4436
}
45-
files[name] = target;
37+
(data[name] as Array<typeof value>).push(value);
4638
}
4739

4840
/**
@@ -93,14 +85,13 @@ export class BodyParser {
9385
* parse multipart form data
9486
*@see https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
9587
*/
96-
private parseMultiPart(string: string, headerBoundary: string | null) {
97-
const body = {},
98-
files = {};
88+
private parseMultiPart(string: string, headerBoundary: string | null): Data {
89+
const result: Data = {};
9990

10091
//if boundary is null, detect it using the last encapsulation boundary format
10192
if (!headerBoundary) {
10293
if (!/^-{2}(-*[a-z0-9]+)-{2}/gim.test(string)) {
103-
return { body, files };
94+
return {};
10495
}
10596
headerBoundary = RegExp.$1;
10697
}
@@ -134,18 +125,18 @@ export class BodyParser {
134125
content,
135126
headers.type.startsWith('text/') ? 'utf8' : 'binary'
136127
);
137-
this.assignFileValue(files, headers.fieldName, {
128+
this.assignDataEntry(result, headers.fieldName, {
138129
name: headers.fileName.replace(/\.\./g, ''),
139130
data,
140131
size: data.byteLength,
141132
type: headers.type,
142133
});
143134
} else if (!headers.isFile) {
144-
this.assignBodyValue(body, headers.fieldName, content);
135+
this.assignDataEntry(result, headers.fieldName, content);
145136
}
146137
});
147138

148-
return { body, files };
139+
return result;
149140
}
150141

151142
/**
@@ -163,26 +154,26 @@ export class BodyParser {
163154
/**
164155
* parse url encoded request body
165156
*/
166-
private parseUrlEncoded(string: string): Data {
167-
const body: Data = {};
157+
private parseUrlEncoded(string: string): Query {
158+
const result: Query = {};
168159
if (string) {
169160
const pairs = string.split('&');
170161
pairs.forEach((pair) => {
171162
const [name, value] = pair.split('=');
172-
this.assignBodyValue(
173-
body,
163+
this.assignDataEntry(
164+
result,
174165
decodeURIComponent(name),
175166
decodeURIComponent(value || '')
176167
);
177168
});
178169
}
179-
return body;
170+
return result;
180171
}
181172

182173
/**
183174
* parse the query parameters in the given url
184175
*/
185-
parseQueryString(url: string): Data {
176+
parseQueryString(url: string): Query {
186177
if (url.indexOf('?') > -1) {
187178
return this.parseUrlEncoded(url.split('?')[1]);
188179
} else {
@@ -207,7 +198,7 @@ export class BodyParser {
207198
*@param {Buffer} buffer - the buffer data
208199
*@param {string} contentType - the request content type
209200
*/
210-
parse(buffer: Buffer, contentType: string): { files: Files; body: Data } {
201+
parse(buffer: Buffer, contentType: string): Data {
211202
const content = buffer.toString('latin1');
212203
const tokens = contentType.split(/;\s*/);
213204

@@ -219,14 +210,14 @@ export class BodyParser {
219210
switch (tokens[0].toLowerCase()) {
220211
case 'text/plain':
221212
case 'application/x-www-form-urlencoded':
222-
return { files: {}, body: this.parseUrlEncoded(content) };
213+
return this.parseUrlEncoded(content);
223214

224215
case 'text/json':
225216
case 'application/json':
226-
return { files: {}, body: this.parseJSON(content) };
217+
return this.parseJSON(content);
227218

228219
default:
229-
return { body: {}, files: {} };
220+
return {};
230221
}
231222
}
232223
}

src/modules/Request.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IncomingMessage } from 'http';
22
import { Http2ServerRequest } from 'http2';
3-
import { Data, Files, Method } from '../@types';
3+
import { Data, Method, Query } from '../@types';
44

55
export type ServerRequest<
66
T extends typeof IncomingMessage | typeof Http2ServerRequest =
@@ -15,11 +15,7 @@ export type ServerRequest<
1515

1616
endedAt: Date | null;
1717

18-
files: Files;
19-
20-
query: Data;
21-
22-
body: Data;
18+
query: Query;
2319

2420
data: Data;
2521

@@ -53,9 +49,7 @@ const createRequestClass = <
5349
RequestClass.prototype.init = function (encrypted) {
5450
this.buffer = Buffer.alloc(0);
5551

56-
this.files = {};
5752
this.query = {};
58-
this.body = {};
5953

6054
// data is a merge of query and body
6155
this.data = {};

src/modules/Server.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -340,18 +340,13 @@ export class Server<
340340
//parse the request body
341341
if (request.buffer.length > 0) {
342342
const contentType = request.headers['content-type'] || 'text/plain';
343+
const data = this.bodyParser.parse(request.buffer, contentType);
343344

344-
//TODO: add support for content encoding
345-
const result = this.bodyParser.parse(request.buffer, contentType);
346-
347-
request.body = result.body;
348-
request.files = result.files;
345+
request.data = {
346+
...request.query,
347+
...data,
348+
};
349349
}
350-
351-
request.data = {
352-
...request.query,
353-
...request.body,
354-
};
355350
}
356351

357352
/**

tests/modules/BodyParser.spec.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,59 +43,56 @@ describe('BodyParser', function () {
4343

4444
describe(`#parse(buffer: Buffer, contentType: string): {files: Files, body: Data}`, function () {
4545
it(`should inspect the given contentType, and parse the buffer data accordingly, passing
46-
data as urlencoded into body if contentType is text/plain or application/x-www-form-urlencoded`, function () {
47-
expect(bodyParser.parse(buffer, 'text/plain').body).toEqual(data);
46+
as urlencoded if contentType is text/plain or application/x-www-form-urlencoded`, function () {
47+
expect(bodyParser.parse(buffer, 'text/plain')).toEqual(data);
4848
expect(
49-
bodyParser.parse(buffer, 'application/x-www-form-urlencoded').body
49+
bodyParser.parse(buffer, 'application/x-www-form-urlencoded')
5050
).toEqual(data);
5151
});
5252

5353
it(`should inspect the given contentType, and parse the buffer data accordingly, passing
54-
data as json encoded into body if contentType is text/json or application/json`, function () {
54+
content as json encoded if contentType is text/json or application/json`, function () {
5555
const buffer = Buffer.from(JSON.stringify(data));
56-
expect(bodyParser.parse(buffer, 'text/json').body).toEqual(data);
57-
expect(bodyParser.parse(buffer, 'application/json').body).toEqual(data);
56+
expect(bodyParser.parse(buffer, 'text/json')).toMatchObject(data);
57+
expect(bodyParser.parse(buffer, 'application/json')).toMatchObject(data);
5858
});
5959

6060
it(`should catch json decode error for erronous json strings and return empty object`, function () {
6161
const erronousJson = "{'age': 20}";
6262
const buffer = Buffer.from(erronousJson);
63-
expect(bodyParser.parse(buffer, 'text/json').body).toEqual({});
63+
expect(bodyParser.parse(buffer, 'text/json')).toEqual({});
6464
});
6565

6666
it(`should inspect the given contentType, and parse the buffer data accordingly, passing
67-
data as multipart form data into body and files object if contentType is
67+
data as multipart form data if contentType is
6868
multipart/form-data`, function () {
6969
const buffer = fs.readFileSync(multipartLogFile);
7070
const contentType = `multipart/form-data; boundary=${multipartBoundary}`;
7171

7272
const result = bodyParser.parse(buffer, contentType);
73-
expect(result.body).toEqual(data);
73+
expect(result).toMatchObject(data);
7474
});
7575

7676
it(`should detect the multipart boundary if not given`, function () {
7777
const buffer = fs.readFileSync(multipartLogFile);
7878
const contentType = `multipart/form-data`;
7979

8080
const result = bodyParser.parse(buffer, contentType);
81-
expect(result.body).toEqual(data);
81+
expect(result).toMatchObject(data);
8282
});
8383

84-
it(`should return empty body and files object if multipart boundary is not given and it
84+
it(`should return empty object if multipart boundary is not given and it
8585
could not be detected`, function () {
8686
const buffer = fs.readFileSync(multipartLogFileNoBoundary);
8787
const contentType = `multipart/form-data`;
8888

8989
const result = bodyParser.parse(buffer, contentType);
90-
expect(result.body).toEqual({});
91-
expect(result.files).toEqual({});
90+
expect(result).toEqual({});
9291
});
9392

94-
it(`should do nothing if contentType is not recognised and return empty body and files
95-
object`, function () {
93+
it(`should do nothing if contentType is not recognised and return object`, function () {
9694
const buffer = Buffer.from(JSON.stringify(data));
97-
expect(bodyParser.parse(buffer, '').body).toEqual({});
98-
expect(bodyParser.parse(buffer, '').files).toEqual({});
95+
expect(bodyParser.parse(buffer, '')).toEqual({});
9996
});
10097
});
10198
});

0 commit comments

Comments
 (0)