structured-fw 1.4.0 → 1.6.0
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 +64 -23
- package/build/system/EventEmitter.js +4 -6
- package/build/system/StructuredError.d.ts +8 -0
- package/build/system/StructuredError.js +40 -0
- package/build/system/bin/structured.js +1 -2
- package/build/system/client/ClientApplication.js +3 -2
- package/build/system/client/ClientComponent.js +21 -13
- package/build/system/client/DataStore.js +2 -4
- package/build/system/client/DataStoreView.js +3 -1
- package/build/system/client/NetRequest.js +7 -2
- package/build/system/server/Application.d.ts +3 -2
- package/build/system/server/Application.js +19 -7
- package/build/system/server/Component.js +15 -7
- package/build/system/server/Components.js +3 -2
- package/build/system/server/Document.d.ts +1 -1
- package/build/system/server/Document.js +10 -7
- package/build/system/server/DocumentHead.js +9 -8
- package/build/system/server/FormValidation.js +121 -123
- package/build/system/server/Handlebars.js +2 -4
- package/build/system/server/Layout.d.ts +2 -2
- package/build/system/server/Layout.js +4 -0
- package/build/system/server/Request.d.ts +1 -11
- package/build/system/server/Request.js +12 -269
- package/build/system/server/RequestContext.d.ts +43 -0
- package/build/system/server/RequestContext.js +322 -0
- package/build/system/server/Session.js +3 -2
- package/build/system/server/dom/DOMNode.js +11 -7
- package/build/system/server/dom/HTMLParser.js +10 -8
- package/build/system/types/application.types.d.ts +1 -1
- package/build/system/types/component.types.d.ts +1 -1
- package/build/system/types/request.types.d.ts +1 -19
- package/package.json +2 -1
|
@@ -1,132 +1,130 @@
|
|
|
1
1
|
export class FormValidation {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
fieldRules = [];
|
|
3
|
+
singleError = false;
|
|
4
|
+
customValidators = [];
|
|
5
|
+
validators = {
|
|
6
|
+
'required': async (data, field) => {
|
|
7
|
+
if (!(field in data)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const value = data[field];
|
|
11
|
+
if (value === null ||
|
|
12
|
+
value === undefined ||
|
|
13
|
+
(typeof value === 'string' && value.trim().length === 0)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
},
|
|
18
|
+
'number': async (data, field) => {
|
|
19
|
+
const value = data[field];
|
|
20
|
+
if (typeof value === 'number') {
|
|
17
21
|
return true;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const value = data[field];
|
|
21
|
-
if (typeof value === 'number') {
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
if (typeof value !== 'string') {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
return /^-?\d+$/.test(value);
|
|
28
|
-
},
|
|
29
|
-
'float': async (data, field) => {
|
|
30
|
-
const value = data[field];
|
|
31
|
-
if (typeof value === 'number') {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
if (typeof value !== 'string') {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
return /^-?\d+\.\d+$/.test(value);
|
|
38
|
-
},
|
|
39
|
-
'numeric': async (data, field, arg, rules) => {
|
|
40
|
-
return await this.validators['number'](data, field, arg, rules) || await this.validators['float'](data, field, arg, rules);
|
|
41
|
-
},
|
|
42
|
-
'min': async (data, field, arg, rules) => {
|
|
43
|
-
const value = data[field];
|
|
44
|
-
if (typeof value === 'number') {
|
|
45
|
-
return value >= arg;
|
|
46
|
-
}
|
|
47
|
-
if (typeof value !== 'string') {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
if (await this.validators['numeric'](data, field, arg, rules)) {
|
|
51
|
-
return parseFloat(value) >= arg;
|
|
52
|
-
}
|
|
22
|
+
}
|
|
23
|
+
if (typeof value !== 'string') {
|
|
53
24
|
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (await this.validators['numeric'](data, field, arg, rules)) {
|
|
64
|
-
return parseFloat(value) <= arg;
|
|
65
|
-
}
|
|
25
|
+
}
|
|
26
|
+
return /^-?\d+$/.test(value);
|
|
27
|
+
},
|
|
28
|
+
'float': async (data, field) => {
|
|
29
|
+
const value = data[field];
|
|
30
|
+
if (typeof value === 'number') {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== 'string') {
|
|
66
34
|
return false;
|
|
67
|
-
},
|
|
68
|
-
'minLength': async (data, field, arg) => {
|
|
69
|
-
const value = data[field];
|
|
70
|
-
if (typeof value !== 'string') {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
return value.length >= arg;
|
|
74
|
-
},
|
|
75
|
-
'maxLength': async (data, field, arg) => {
|
|
76
|
-
const value = data[field];
|
|
77
|
-
if (typeof value !== 'string') {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
return value.length <= arg;
|
|
81
|
-
},
|
|
82
|
-
'alphanumeric': async (data, field) => {
|
|
83
|
-
const value = data[field];
|
|
84
|
-
if (typeof value !== 'string') {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
return /^[a-zA-Z0-9]+$/.test(value);
|
|
88
|
-
},
|
|
89
|
-
'validEmail': async (data, field) => {
|
|
90
|
-
const value = data[field];
|
|
91
|
-
if (typeof value !== 'string') {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
return /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test(value);
|
|
95
35
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return `${fieldHuman} has to be a decimal number`;
|
|
106
|
-
},
|
|
107
|
-
'numeric': (fieldHuman) => {
|
|
108
|
-
return `${fieldHuman} has to contain a numeric value`;
|
|
109
|
-
},
|
|
110
|
-
'min': (fieldHuman, data, field, arg) => {
|
|
111
|
-
return `${fieldHuman} has to be a value greater than ${arg}`;
|
|
112
|
-
},
|
|
113
|
-
'max': (fieldHuman, data, field, arg) => {
|
|
114
|
-
return `${fieldHuman} has to be a value lower than ${arg}`;
|
|
115
|
-
},
|
|
116
|
-
'minLength': (fieldHuman, data, field, arg) => {
|
|
117
|
-
return `${fieldHuman} has to contain at least ${arg} characters`;
|
|
118
|
-
},
|
|
119
|
-
'maxLength': (fieldHuman, data, field, arg) => {
|
|
120
|
-
return `${fieldHuman} has to contain no more than ${arg} characters`;
|
|
121
|
-
},
|
|
122
|
-
'alphanumeric': (fieldHuman) => {
|
|
123
|
-
return `${fieldHuman} can contain only letter and numbers`;
|
|
124
|
-
},
|
|
125
|
-
'validEmail': () => {
|
|
126
|
-
return `Please enter a valid email address`;
|
|
36
|
+
return /^-?\d+\.\d+$/.test(value);
|
|
37
|
+
},
|
|
38
|
+
'numeric': async (data, field, arg, rules) => {
|
|
39
|
+
return await this.validators['number'](data, field, arg, rules) || await this.validators['float'](data, field, arg, rules);
|
|
40
|
+
},
|
|
41
|
+
'min': async (data, field, arg, rules) => {
|
|
42
|
+
const value = data[field];
|
|
43
|
+
if (typeof value === 'number') {
|
|
44
|
+
return value >= arg;
|
|
127
45
|
}
|
|
128
|
-
|
|
129
|
-
|
|
46
|
+
if (typeof value !== 'string') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (await this.validators['numeric'](data, field, arg, rules)) {
|
|
50
|
+
return parseFloat(value) >= arg;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
},
|
|
54
|
+
'max': async (data, field, arg, rules) => {
|
|
55
|
+
const value = data[field];
|
|
56
|
+
if (typeof value === 'number') {
|
|
57
|
+
return value <= arg;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value !== 'string') {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (await this.validators['numeric'](data, field, arg, rules)) {
|
|
63
|
+
return parseFloat(value) <= arg;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
},
|
|
67
|
+
'minLength': async (data, field, arg) => {
|
|
68
|
+
const value = data[field];
|
|
69
|
+
if (typeof value !== 'string') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return value.length >= arg;
|
|
73
|
+
},
|
|
74
|
+
'maxLength': async (data, field, arg) => {
|
|
75
|
+
const value = data[field];
|
|
76
|
+
if (typeof value !== 'string') {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return value.length <= arg;
|
|
80
|
+
},
|
|
81
|
+
'alphanumeric': async (data, field) => {
|
|
82
|
+
const value = data[field];
|
|
83
|
+
if (typeof value !== 'string') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return /^[a-zA-Z0-9]+$/.test(value);
|
|
87
|
+
},
|
|
88
|
+
'validEmail': async (data, field) => {
|
|
89
|
+
const value = data[field];
|
|
90
|
+
if (typeof value !== 'string') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test(value);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
decorators = {
|
|
97
|
+
'required': (fieldHuman) => {
|
|
98
|
+
return `${fieldHuman} is required`;
|
|
99
|
+
},
|
|
100
|
+
'number': (fieldHuman) => {
|
|
101
|
+
return `${fieldHuman} has to be a whole number`;
|
|
102
|
+
},
|
|
103
|
+
'float': (fieldHuman) => {
|
|
104
|
+
return `${fieldHuman} has to be a decimal number`;
|
|
105
|
+
},
|
|
106
|
+
'numeric': (fieldHuman) => {
|
|
107
|
+
return `${fieldHuman} has to contain a numeric value`;
|
|
108
|
+
},
|
|
109
|
+
'min': (fieldHuman, data, field, arg) => {
|
|
110
|
+
return `${fieldHuman} has to be a value greater than ${arg}`;
|
|
111
|
+
},
|
|
112
|
+
'max': (fieldHuman, data, field, arg) => {
|
|
113
|
+
return `${fieldHuman} has to be a value lower than ${arg}`;
|
|
114
|
+
},
|
|
115
|
+
'minLength': (fieldHuman, data, field, arg) => {
|
|
116
|
+
return `${fieldHuman} has to contain at least ${arg} characters`;
|
|
117
|
+
},
|
|
118
|
+
'maxLength': (fieldHuman, data, field, arg) => {
|
|
119
|
+
return `${fieldHuman} has to contain no more than ${arg} characters`;
|
|
120
|
+
},
|
|
121
|
+
'alphanumeric': (fieldHuman) => {
|
|
122
|
+
return `${fieldHuman} can contain only letter and numbers`;
|
|
123
|
+
},
|
|
124
|
+
'validEmail': () => {
|
|
125
|
+
return `Please enter a valid email address`;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
130
128
|
addRule(fieldName, nameHumanReadable, rules) {
|
|
131
129
|
const rule = {
|
|
132
130
|
field: [fieldName, nameHumanReadable],
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { default as HandlebarsInstance } from 'handlebars';
|
|
2
2
|
export class Handlebars {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
this.helpers = {};
|
|
6
|
-
}
|
|
3
|
+
instance = HandlebarsInstance;
|
|
4
|
+
helpers = {};
|
|
7
5
|
register(name, helper) {
|
|
8
6
|
this.helpers[name] = helper;
|
|
9
7
|
this.instance.registerHelper(name, helper);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LooseObject } from '../types/general.types.js';
|
|
2
|
-
import { RequestContext } from "../types/request.types.js";
|
|
3
2
|
import { Application } from "./Application.js";
|
|
4
3
|
import { Document } from "./Document.js";
|
|
4
|
+
import { RequestContext } from './RequestContext.js';
|
|
5
5
|
export declare class Layout {
|
|
6
6
|
layoutComponent: string;
|
|
7
7
|
app: Application;
|
|
@@ -9,5 +9,5 @@ export declare class Layout {
|
|
|
9
9
|
attributes: Record<string, string>;
|
|
10
10
|
constructor(app: Application, layoutComponent: string, language?: string, attributes?: Record<string, string>);
|
|
11
11
|
private layoutComponentExists;
|
|
12
|
-
document(ctx: RequestContext
|
|
12
|
+
document(ctx: RequestContext<any>, title: string, componentName: string, data?: LooseObject, attributes?: Record<string, string>): Promise<Document>;
|
|
13
13
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Component } from "./Component.js";
|
|
2
2
|
import { Document } from "./Document.js";
|
|
3
3
|
export class Layout {
|
|
4
|
+
layoutComponent;
|
|
5
|
+
app;
|
|
6
|
+
language;
|
|
7
|
+
attributes;
|
|
4
8
|
constructor(app, layoutComponent, language = 'en', attributes) {
|
|
5
9
|
this.app = app;
|
|
6
10
|
this.layoutComponent = layoutComponent;
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { LooseObject } from '../types/general.types.js';
|
|
3
|
-
import { RequestMethod,
|
|
3
|
+
import { RequestMethod, RequestCallback } from "../types/request.types.js";
|
|
4
4
|
import { Application } from "./Application.js";
|
|
5
|
-
import { Document } from "./Document.js";
|
|
6
5
|
export declare class Request {
|
|
7
6
|
private app;
|
|
8
7
|
constructor(app: Application);
|
|
9
|
-
pageNotFoundCallback: RequestCallback<void | Document, LooseObject>;
|
|
10
8
|
private readonly handlers;
|
|
11
9
|
on<R extends any, Body extends LooseObject | undefined = LooseObject>(methods: RequestMethod | Array<RequestMethod>, pattern: string | RegExp | Array<string | RegExp>, callback: RequestCallback<R, Body>, scope?: any, isStaticAsset?: boolean): void;
|
|
12
10
|
private getHandler;
|
|
13
11
|
handle(request: IncomingMessage, response: ServerResponse): Promise<void>;
|
|
14
|
-
private extractURIArguments;
|
|
15
12
|
private patternToSegments;
|
|
16
|
-
private parseBody;
|
|
17
|
-
private dataRaw;
|
|
18
|
-
redirect(response: ServerResponse, to: string, statusCode?: number): void;
|
|
19
13
|
loadHandlers(basePath?: string): Promise<void>;
|
|
20
|
-
static queryStringDecode(queryString: string, initialValue?: PostedDataDecoded, trimValues?: boolean): PostedDataDecoded;
|
|
21
|
-
static parseBodyMultipart(bodyRaw: string, boundary: string): PostedDataDecoded;
|
|
22
|
-
static multipartBodyFiles(bodyRaw: string, boundary: string): Record<string, RequestBodyFile>;
|
|
23
|
-
private sendResponse;
|
|
24
14
|
}
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import zlib from "node:zlib";
|
|
3
|
+
import { RequestContext } from "./RequestContext.js";
|
|
4
|
+
import { StructuredError } from "../StructuredError.js";
|
|
7
5
|
export class Request {
|
|
6
|
+
app;
|
|
8
7
|
constructor(app) {
|
|
9
|
-
this.pageNotFoundCallback = async ({ response }) => {
|
|
10
|
-
response.statusCode = 404;
|
|
11
|
-
response.write('Page not found');
|
|
12
|
-
response.end();
|
|
13
|
-
};
|
|
14
|
-
this.handlers = [];
|
|
15
8
|
this.app = app;
|
|
16
9
|
}
|
|
10
|
+
handlers = [];
|
|
17
11
|
on(methods, pattern, callback, scope, isStaticAsset = false) {
|
|
18
12
|
if (!(methods instanceof Array)) {
|
|
19
13
|
methods = [methods];
|
|
@@ -90,128 +84,23 @@ export class Request {
|
|
|
90
84
|
if (this.app.config.url.removeTrailingSlash && uri.length > 1 && uri.endsWith('/')) {
|
|
91
85
|
uri = uri.substring(0, uri.length - 1);
|
|
92
86
|
}
|
|
93
|
-
let getArgs = {};
|
|
94
87
|
if (uri.indexOf('?') > -1) {
|
|
95
88
|
const uriParts = uri.split('?');
|
|
96
89
|
uri = uriParts[0];
|
|
97
|
-
getArgs = queryStringDecode(uriParts[1]);
|
|
98
90
|
}
|
|
99
91
|
const handler = this.getHandler(uri, requestMethod);
|
|
100
|
-
|
|
101
|
-
request,
|
|
102
|
-
|
|
103
|
-
handler,
|
|
104
|
-
args: {},
|
|
105
|
-
data: {},
|
|
106
|
-
body: undefined,
|
|
107
|
-
getArgs,
|
|
108
|
-
cookies: this.app.cookies.parse(request),
|
|
109
|
-
timeStart: new Date().getTime(),
|
|
110
|
-
isAjax: request.headers['x-requested-with'] == 'xmlhttprequest',
|
|
111
|
-
respondWith: async (data) => {
|
|
112
|
-
if (typeof data === 'string' || Buffer.isBuffer(data)) {
|
|
113
|
-
this.sendResponse(request, response, data, 'text/plain; charset=utf-8');
|
|
114
|
-
}
|
|
115
|
-
else if (typeof data === 'number') {
|
|
116
|
-
this.sendResponse(request, response, data.toString(), 'text/plain; charset=utf-8');
|
|
117
|
-
}
|
|
118
|
-
else if (data instanceof Document) {
|
|
119
|
-
this.sendResponse(request, response, await data.toString(), 'text/html; charset=utf-8');
|
|
120
|
-
}
|
|
121
|
-
else if (data === undefined || data === null) {
|
|
122
|
-
this.sendResponse(request, response, '', 'text/plain; charset=utf-8');
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
this.sendResponse(request, response, JSON.stringify(data, null, 4), 'application/json; charset=utf-8');
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
redirect: (to, statusCode = 302) => {
|
|
129
|
-
this.redirect(response, to, statusCode);
|
|
130
|
-
},
|
|
131
|
-
show404: async () => {
|
|
132
|
-
this.app.emit('pageNotFound', context);
|
|
133
|
-
response.statusCode = 404;
|
|
134
|
-
const res = await this.pageNotFoundCallback.apply(this.app, [context]);
|
|
135
|
-
if (res instanceof Document) {
|
|
136
|
-
await context.respondWith(res);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
if (handler !== null) {
|
|
141
|
-
if (!handler.staticAsset) {
|
|
142
|
-
const results = await this.app.emit('beforeRequestHandler', context);
|
|
143
|
-
if (results.includes(false)) {
|
|
144
|
-
context.response.end();
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
await this.parseBody(context);
|
|
149
|
-
}
|
|
150
|
-
catch (e) {
|
|
151
|
-
console.error(`Error parsing request body: ${e.message}`);
|
|
152
|
-
}
|
|
153
|
-
const URIArgs = this.extractURIArguments(uri, handler.match);
|
|
154
|
-
context.args = URIArgs;
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const response = await handler.callback.apply(handler.scope, [context]);
|
|
158
|
-
if (!context.response.headersSent) {
|
|
159
|
-
await context.respondWith(response);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
catch (e) {
|
|
163
|
-
console.log('Error executing request handler ', e, handler.callback.toString());
|
|
164
|
-
}
|
|
165
|
-
if (!handler.staticAsset) {
|
|
166
|
-
await this.app.emit('afterRequestHandler', context);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
let staticAsset = false;
|
|
171
|
-
if (this.app.config.url.isAsset(context.request.url || '')) {
|
|
172
|
-
const basePath = context.request.url?.startsWith('/assets/ts/') ? './' : '../';
|
|
173
|
-
const assetPath = path.resolve(basePath + context.request.url);
|
|
174
|
-
if (existsSync(assetPath)) {
|
|
175
|
-
await this.app.emit('beforeAssetAccess', context);
|
|
176
|
-
const extension = (context.request.url || '').split('.').pop();
|
|
177
|
-
let contentType = 'application/javascript';
|
|
178
|
-
if (extension) {
|
|
179
|
-
const typeByExtension = this.app.contentType(extension);
|
|
180
|
-
if (typeByExtension) {
|
|
181
|
-
contentType = typeByExtension;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
this.sendResponse(request, response, readFileSync(assetPath), contentType);
|
|
185
|
-
staticAsset = true;
|
|
186
|
-
await this.app.emit('afterAssetAccess', context);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (!staticAsset) {
|
|
190
|
-
await context.show404();
|
|
191
|
-
}
|
|
92
|
+
try {
|
|
93
|
+
const ctx = new RequestContext(this.app, request, response, handler);
|
|
94
|
+
await ctx.exec();
|
|
192
95
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (match instanceof RegExp) {
|
|
197
|
-
const matches = match.exec(uri);
|
|
198
|
-
if (matches) {
|
|
199
|
-
return {
|
|
200
|
-
matches
|
|
201
|
-
};
|
|
96
|
+
catch (e) {
|
|
97
|
+
if (e instanceof StructuredError) {
|
|
98
|
+
e.log();
|
|
202
99
|
}
|
|
203
100
|
else {
|
|
204
|
-
|
|
101
|
+
throw e;
|
|
205
102
|
}
|
|
206
103
|
}
|
|
207
|
-
const uriArgs = {};
|
|
208
|
-
const segments = uri.split('/');
|
|
209
|
-
match.forEach((segmentPattern, i) => {
|
|
210
|
-
if (segmentPattern.name) {
|
|
211
|
-
uriArgs[segmentPattern.name] = segmentPattern.type === 'number' ? parseInt(segments[i]) : segments[i];
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
return uriArgs;
|
|
215
104
|
}
|
|
216
105
|
patternToSegments(pattern) {
|
|
217
106
|
const segments = [];
|
|
@@ -244,81 +133,6 @@ export class Request {
|
|
|
244
133
|
});
|
|
245
134
|
return segments;
|
|
246
135
|
}
|
|
247
|
-
async parseBody(ctx) {
|
|
248
|
-
if (ctx.request.headers['content-type']) {
|
|
249
|
-
ctx.bodyRaw = await this.dataRaw(ctx.request);
|
|
250
|
-
if (ctx.request.headers['content-type'].indexOf('urlencoded') > -1) {
|
|
251
|
-
const bodyRaw = ctx.bodyRaw.toString('utf-8');
|
|
252
|
-
try {
|
|
253
|
-
ctx.body = queryStringDecode(bodyRaw);
|
|
254
|
-
}
|
|
255
|
-
catch (e) {
|
|
256
|
-
console.error(`Error parsing urlencoded request body for request to ${ctx.request.url}, (${e.message}).
|
|
257
|
-
Raw data:
|
|
258
|
-
${bodyRaw}
|
|
259
|
-
|
|
260
|
-
`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
else if (ctx.request.headers['content-type'].indexOf('multipart/form-data') > -1) {
|
|
264
|
-
let boundary = /^multipart\/form-data; boundary=(.+)$/.exec(ctx.request.headers['content-type']);
|
|
265
|
-
if (boundary) {
|
|
266
|
-
boundary = `--${boundary[1]}`;
|
|
267
|
-
try {
|
|
268
|
-
ctx.body = Request.parseBodyMultipart(ctx.bodyRaw.toString('utf-8'), boundary);
|
|
269
|
-
}
|
|
270
|
-
catch (e) {
|
|
271
|
-
console.error(`Error parsing multipart request body for request to ${ctx.request.url}, (${e.message}).
|
|
272
|
-
Raw data:
|
|
273
|
-
${ctx.bodyRaw.toString('utf-8')}
|
|
274
|
-
|
|
275
|
-
`);
|
|
276
|
-
}
|
|
277
|
-
try {
|
|
278
|
-
ctx.files = Request.multipartBodyFiles(ctx.bodyRaw.toString('binary'), boundary);
|
|
279
|
-
}
|
|
280
|
-
catch (e) {
|
|
281
|
-
console.error(`Error parsing multipart request body files for request to ${ctx.request.url}, (${e.message}).
|
|
282
|
-
Raw data:
|
|
283
|
-
${ctx.bodyRaw.toString('utf-8')}
|
|
284
|
-
|
|
285
|
-
`);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
else if (ctx.request.headers['content-type'].indexOf('application/json') > -1) {
|
|
290
|
-
try {
|
|
291
|
-
ctx.body = JSON.parse(ctx.bodyRaw.toString());
|
|
292
|
-
}
|
|
293
|
-
catch (e) {
|
|
294
|
-
ctx.body = undefined;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
dataRaw(request) {
|
|
301
|
-
const chunks = [];
|
|
302
|
-
return new Promise((resolve, reject) => {
|
|
303
|
-
request.on('data', (chunk) => {
|
|
304
|
-
chunks.push(chunk);
|
|
305
|
-
});
|
|
306
|
-
request.on('close', () => {
|
|
307
|
-
const size = chunks.reduce((prev, curr) => {
|
|
308
|
-
return prev + curr.length;
|
|
309
|
-
}, 0);
|
|
310
|
-
const data = Buffer.concat(chunks, size);
|
|
311
|
-
resolve(data);
|
|
312
|
-
});
|
|
313
|
-
request.on('error', (e) => {
|
|
314
|
-
reject(e);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
redirect(response, to, statusCode = 302) {
|
|
319
|
-
response.setHeader('Location', to);
|
|
320
|
-
response.writeHead(statusCode);
|
|
321
|
-
}
|
|
322
136
|
async loadHandlers(basePath) {
|
|
323
137
|
let routesPath;
|
|
324
138
|
if (basePath) {
|
|
@@ -348,76 +162,5 @@ export class Request {
|
|
|
348
162
|
}
|
|
349
163
|
}
|
|
350
164
|
}
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
static queryStringDecode(queryString, initialValue = {}, trimValues = true) {
|
|
354
|
-
return queryStringDecode(queryString, initialValue, trimValues);
|
|
355
|
-
}
|
|
356
|
-
static parseBodyMultipart(bodyRaw, boundary) {
|
|
357
|
-
const pairsRaw = bodyRaw.split(boundary);
|
|
358
|
-
const pairs = pairsRaw.map((pair) => {
|
|
359
|
-
const parts = pair.split(/\r?\n\r?\n/, 2).filter((part) => { return part.length > 0; });
|
|
360
|
-
if (parts.length > 0) {
|
|
361
|
-
const header = parts[0];
|
|
362
|
-
const data = typeof parts[1] === 'string' ? parts[1].trim() : '';
|
|
363
|
-
const headerParts = /Content-Disposition: form-data; name="([^\r\n"]+)"/m.exec(header);
|
|
364
|
-
if (headerParts) {
|
|
365
|
-
return {
|
|
366
|
-
key: headerParts[1],
|
|
367
|
-
value: data
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return null;
|
|
372
|
-
});
|
|
373
|
-
const urlEncoded = pairs.reduce((prev, curr) => {
|
|
374
|
-
if (curr !== null) {
|
|
375
|
-
prev.push(`${curr.key}=${encodeURIComponent(curr.value.replaceAll('&', '%26'))}`);
|
|
376
|
-
}
|
|
377
|
-
return prev;
|
|
378
|
-
}, []).join('&');
|
|
379
|
-
return queryStringDecode(urlEncoded);
|
|
380
|
-
}
|
|
381
|
-
static multipartBodyFiles(bodyRaw, boundary) {
|
|
382
|
-
let files = {};
|
|
383
|
-
const pairsRaw = bodyRaw.split(boundary);
|
|
384
|
-
pairsRaw.map((pair) => {
|
|
385
|
-
const parts = /Content-Disposition: form-data; name="(.+?)"; filename="(.+?)"\r\nContent-Type: (.*)\r\n\r\n([\s\S]+)$/m.exec(pair);
|
|
386
|
-
if (parts) {
|
|
387
|
-
const file = {
|
|
388
|
-
data: Buffer.from(parts[4].substring(0, parts[4].length - 2).trim(), 'binary'),
|
|
389
|
-
fileName: parts[2],
|
|
390
|
-
type: parts[3]
|
|
391
|
-
};
|
|
392
|
-
files = mergeDeep(files, queryStringDecodedSetValue(parts[1], file));
|
|
393
|
-
}
|
|
394
|
-
return null;
|
|
395
|
-
});
|
|
396
|
-
return files;
|
|
397
|
-
}
|
|
398
|
-
sendResponse(request, response, buffer, contentType) {
|
|
399
|
-
const mimeType = contentType.split(';')[0];
|
|
400
|
-
const gzipResponse = this.app.config.gzip.enabled &&
|
|
401
|
-
this.app.config.gzip.types.includes(mimeType) &&
|
|
402
|
-
request.headers['accept-encoding']?.includes('gzip') &&
|
|
403
|
-
buffer.length >= this.app.config.gzip.minSize;
|
|
404
|
-
if (!response.hasHeader('Content-Type')) {
|
|
405
|
-
response.setHeader('Content-Type', contentType);
|
|
406
|
-
}
|
|
407
|
-
if (typeof buffer === 'string') {
|
|
408
|
-
buffer = Buffer.from(buffer, 'utf-8');
|
|
409
|
-
}
|
|
410
|
-
if (gzipResponse) {
|
|
411
|
-
response.setHeader('Content-Encoding', 'gzip');
|
|
412
|
-
const compressed = zlib.gzipSync(buffer, {
|
|
413
|
-
level: this.app.config.gzip.compressionLevel
|
|
414
|
-
});
|
|
415
|
-
response.setHeader('Content-Length', compressed.length);
|
|
416
|
-
response.write(compressed);
|
|
417
|
-
}
|
|
418
|
-
else {
|
|
419
|
-
response.setHeader('Content-Length', buffer.length);
|
|
420
|
-
response.write(buffer);
|
|
421
|
-
}
|
|
422
165
|
}
|
|
423
166
|
}
|