structured-fw 1.4.0 → 1.5.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 +61 -22
- package/build/system/server/Application.d.ts +2 -1
- package/build/system/server/Application.js +7 -0
- package/build/system/server/Document.d.ts +1 -1
- package/build/system/server/Layout.d.ts +2 -2
- package/build/system/server/Request.d.ts +2 -10
- package/build/system/server/Request.js +3 -268
- package/build/system/server/RequestContext.d.ts +41 -0
- package/build/system/server/RequestContext.js +307 -0
- 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
package/README.md
CHANGED
|
@@ -29,6 +29,13 @@ npm init -y
|
|
|
29
29
|
npm install @types/node
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Set type to "module" in package.json
|
|
33
|
+
```
|
|
34
|
+
...
|
|
35
|
+
"type": "module",
|
|
36
|
+
...
|
|
37
|
+
```
|
|
38
|
+
|
|
32
39
|
*If you have TypeScript installed globally then you can skip the following*\
|
|
33
40
|
`npm install --save-dev typescript`
|
|
34
41
|
|
|
@@ -258,44 +265,66 @@ Route file name has no effect on how the route (request handler) behaves, the on
|
|
|
258
265
|
RequestContext is created for every received request. It contains all the data related to the request, as well as data you include in `RequestContextData`.
|
|
259
266
|
All request handlers receive a `RequestContext` as the first argument. Your component server side part also receives `RequestContext` as the second argument.
|
|
260
267
|
```
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
268
|
+
class RequestContext<Body extends LooseObject | undefined = LooseObject> = {
|
|
269
|
+
app: Application;
|
|
270
|
+
uri: string;
|
|
271
|
+
|
|
272
|
+
request: IncomingMessage;
|
|
273
|
+
response: ServerResponse;
|
|
266
274
|
|
|
267
|
-
|
|
275
|
+
// captured URI arguments
|
|
276
|
+
// for example if the requested URI was /users/8 and the pattern was /users/(userId:num)
|
|
277
|
+
// args will be { userId: 8 }
|
|
278
|
+
args: URIArguments;
|
|
279
|
+
|
|
280
|
+
// request cookies parsed into an object
|
|
281
|
+
cookies: Record<string, string>;
|
|
268
282
|
|
|
269
283
|
// POSTed data, parsed to object
|
|
270
|
-
body?: LooseObject
|
|
284
|
+
body?: LooseObject;
|
|
271
285
|
|
|
272
|
-
bodyRaw?: Buffer
|
|
286
|
+
bodyRaw?: Buffer;
|
|
273
287
|
|
|
274
288
|
// files extracted from request body
|
|
275
|
-
files?: Record<string, RequestBodyRecordValue
|
|
289
|
+
files?: Record<string, RequestBodyRecordValue>;
|
|
276
290
|
|
|
277
291
|
// user defined data
|
|
278
|
-
data: RequestContextData
|
|
292
|
+
data: RequestContextData;
|
|
279
293
|
|
|
280
|
-
// if session is started and user has visited any page
|
|
281
|
-
sessionId?: string
|
|
294
|
+
// defined if session is started and user has visited any page
|
|
295
|
+
sessionId?: string;
|
|
282
296
|
|
|
283
|
-
// true if x-requested-with header is
|
|
284
|
-
isAjax: boolean
|
|
297
|
+
// true if x-requested-with header value is 'xmlhttprequest'
|
|
298
|
+
isAjax: () => boolean;
|
|
285
299
|
|
|
286
300
|
// time when request was received (unix timestamp in milliseconds)
|
|
287
|
-
timeStart: number
|
|
301
|
+
timeStart: number;
|
|
302
|
+
|
|
303
|
+
// URL GET arguments, for example if the URI isss /users?id=8
|
|
304
|
+
// getArgs would be { id: '8' }
|
|
305
|
+
getArgs: PostedDataDecoded;
|
|
306
|
+
|
|
307
|
+
// create a Document and load given component
|
|
308
|
+
createDocument: (title: string, component: string, data?: LooseObject) => Promise<Document>;
|
|
288
309
|
|
|
289
|
-
//
|
|
290
|
-
|
|
310
|
+
// create a Document using provided layout
|
|
311
|
+
layoutDocument: layoutDocument(
|
|
312
|
+
layout: Layout,
|
|
313
|
+
title: string,
|
|
314
|
+
component: string,
|
|
315
|
+
data?: LooseObject,
|
|
316
|
+
attributes?: Record<string, string>
|
|
317
|
+
) => Promise<Document>;
|
|
291
318
|
|
|
292
319
|
// send given data as a response
|
|
293
|
-
respondWith: (data: any) => Promise<void
|
|
320
|
+
respondWith: (data: any) => Promise<void>;
|
|
294
321
|
|
|
295
|
-
// redirect to given
|
|
296
|
-
redirect: (to: string, statusCode?: number) => void
|
|
322
|
+
// redirect to given URI, with given statusCode (default 302)
|
|
323
|
+
redirect: (to: string, statusCode?: number) => void;
|
|
297
324
|
|
|
298
|
-
// show a 404 page
|
|
325
|
+
// show a 404 page and send a 404 status code
|
|
326
|
+
// by default it will be a page with content "Page not found"
|
|
327
|
+
// to change this you can set app.request.pageNotFoundCallback to a function that returns a Document
|
|
299
328
|
show404: () => Promise<void>
|
|
300
329
|
}
|
|
301
330
|
```
|
|
@@ -371,6 +400,15 @@ app.request.on('GET', '/home', async (ctx) => {
|
|
|
371
400
|
});
|
|
372
401
|
```
|
|
373
402
|
|
|
403
|
+
Above code could be simplified using RequestContext.createDocument method as:
|
|
404
|
+
```
|
|
405
|
+
app.request.on('GET', '/home', async (ctx) => {
|
|
406
|
+
return await ctx.createDocument('Home', 'Home');
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Much shorter, but slightly confusing due to bad example (both arguments being 'Home'). First argument is the document title, second argument is the name of the component you want to load.
|
|
411
|
+
|
|
374
412
|
> [!TIP]
|
|
375
413
|
> Since version 0.8.4 Document extends EventEmitter, and "componentCreated" event is emitted whenever a component instance is created within the Document.\
|
|
376
414
|
> This makes the following possible:
|
|
@@ -445,7 +483,8 @@ That was the simplest possible example, let's make it more interesting by adding
|
|
|
445
483
|
Create a new file `/app/views/HelloWorld/HelloWorld.ts` (server side component code):
|
|
446
484
|
```
|
|
447
485
|
import { Application } from 'structured-fw/Application';
|
|
448
|
-
import { ComponentScaffold
|
|
486
|
+
import { ComponentScaffold } from 'structured-fw/Types';
|
|
487
|
+
import { RequestContext } from 'structured-fw/RequestContext';
|
|
449
488
|
|
|
450
489
|
type ComponentInput = {
|
|
451
490
|
name: string,
|
|
@@ -2,13 +2,13 @@ import { Server } from 'node:http';
|
|
|
2
2
|
import { ApplicationEvents } from '../types/application.types.js';
|
|
3
3
|
import { StructuredConfig } from '../types/structured.types.js';
|
|
4
4
|
import { LooseObject } from '../types/general.types.js';
|
|
5
|
-
import { RequestContext } from "../types/request.types.js";
|
|
6
5
|
import { Document } from './Document.js';
|
|
7
6
|
import { Components } from './Components.js';
|
|
8
7
|
import { Session } from './Session.js';
|
|
9
8
|
import { Request } from './Request.js';
|
|
10
9
|
import { Handlebars } from './Handlebars.js';
|
|
11
10
|
import { Cookies } from './Cookies.js';
|
|
11
|
+
import { RequestContext } from './RequestContext.js';
|
|
12
12
|
export declare class Application {
|
|
13
13
|
readonly config: StructuredConfig;
|
|
14
14
|
initialized: boolean;
|
|
@@ -32,6 +32,7 @@ export declare class Application {
|
|
|
32
32
|
contentType(extension: string): string | false;
|
|
33
33
|
registerPlugin<Opt extends Readonly<LooseObject>>(callback: (app: Application, options: Opt) => void | Promise<void>, opts: NoInfer<Opt>): Promise<void>;
|
|
34
34
|
private respondWithComponent;
|
|
35
|
+
directory(uri?: string): string;
|
|
35
36
|
memoryUsage(): NodeJS.MemoryUsage;
|
|
36
37
|
printMemoryUsage(): void;
|
|
37
38
|
}
|
|
@@ -167,6 +167,13 @@ export class Application {
|
|
|
167
167
|
}
|
|
168
168
|
return false;
|
|
169
169
|
}
|
|
170
|
+
directory(uri) {
|
|
171
|
+
const dir = path.resolve('../');
|
|
172
|
+
if (typeof uri === 'string') {
|
|
173
|
+
return path.resolve(dir, uri.startsWith('/') ? uri.substring(1) : uri);
|
|
174
|
+
}
|
|
175
|
+
return dir;
|
|
176
|
+
}
|
|
170
177
|
memoryUsage() {
|
|
171
178
|
return process.memoryUsage();
|
|
172
179
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ServerResponse } from 'node:http';
|
|
2
2
|
import { LooseObject } from '../types/general.types.js';
|
|
3
3
|
import { Initializers } from '../types/component.types.js';
|
|
4
|
-
import { RequestContext } from "../types/request.types.js";
|
|
5
4
|
import { Application } from './Application.js';
|
|
6
5
|
import { DocumentHead } from './DocumentHead.js';
|
|
7
6
|
import { Component } from './Component.js';
|
|
7
|
+
import { RequestContext } from './RequestContext.js';
|
|
8
8
|
export declare class Document extends Component<{
|
|
9
9
|
'componentCreated': Component;
|
|
10
10
|
'beforeRender': void;
|
|
@@ -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,24 +1,16 @@
|
|
|
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
5
|
import { Document } from "./Document.js";
|
|
6
6
|
export declare class Request {
|
|
7
7
|
private app;
|
|
8
|
+
pageNotFoundCallback: RequestCallback<void | Document, LooseObject | undefined>;
|
|
8
9
|
constructor(app: Application);
|
|
9
|
-
pageNotFoundCallback: RequestCallback<void | Document, LooseObject>;
|
|
10
10
|
private readonly handlers;
|
|
11
11
|
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
12
|
private getHandler;
|
|
13
13
|
handle(request: IncomingMessage, response: ServerResponse): Promise<void>;
|
|
14
|
-
private extractURIArguments;
|
|
15
14
|
private patternToSegments;
|
|
16
|
-
private parseBody;
|
|
17
|
-
private dataRaw;
|
|
18
|
-
redirect(response: ServerResponse, to: string, statusCode?: number): void;
|
|
19
15
|
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
16
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
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 { Document } from "./Document.js";
|
|
6
|
-
import zlib from "node:zlib";
|
|
3
|
+
import { RequestContext } from "./RequestContext.js";
|
|
7
4
|
export class Request {
|
|
8
5
|
constructor(app) {
|
|
9
6
|
this.pageNotFoundCallback = async ({ response }) => {
|
|
@@ -90,128 +87,12 @@ export class Request {
|
|
|
90
87
|
if (this.app.config.url.removeTrailingSlash && uri.length > 1 && uri.endsWith('/')) {
|
|
91
88
|
uri = uri.substring(0, uri.length - 1);
|
|
92
89
|
}
|
|
93
|
-
let getArgs = {};
|
|
94
90
|
if (uri.indexOf('?') > -1) {
|
|
95
91
|
const uriParts = uri.split('?');
|
|
96
92
|
uri = uriParts[0];
|
|
97
|
-
getArgs = queryStringDecode(uriParts[1]);
|
|
98
93
|
}
|
|
99
94
|
const handler = this.getHandler(uri, requestMethod);
|
|
100
|
-
|
|
101
|
-
request,
|
|
102
|
-
response,
|
|
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
|
-
}
|
|
192
|
-
}
|
|
193
|
-
response.end();
|
|
194
|
-
}
|
|
195
|
-
extractURIArguments(uri, match) {
|
|
196
|
-
if (match instanceof RegExp) {
|
|
197
|
-
const matches = match.exec(uri);
|
|
198
|
-
if (matches) {
|
|
199
|
-
return {
|
|
200
|
-
matches
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
return {};
|
|
205
|
-
}
|
|
206
|
-
}
|
|
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;
|
|
95
|
+
new RequestContext(this.app, request, response, handler, this.pageNotFoundCallback);
|
|
215
96
|
}
|
|
216
97
|
patternToSegments(pattern) {
|
|
217
98
|
const segments = [];
|
|
@@ -244,81 +125,6 @@ export class Request {
|
|
|
244
125
|
});
|
|
245
126
|
return segments;
|
|
246
127
|
}
|
|
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
128
|
async loadHandlers(basePath) {
|
|
323
129
|
let routesPath;
|
|
324
130
|
if (basePath) {
|
|
@@ -348,76 +154,5 @@ export class Request {
|
|
|
348
154
|
}
|
|
349
155
|
}
|
|
350
156
|
}
|
|
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
157
|
}
|
|
423
158
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { LooseObject, PostedDataDecoded, RequestBodyRecordValue, RequestCallback, RequestHandler, URIArguments } from "../Types.js";
|
|
3
|
+
import { Application } from "./Application.js";
|
|
4
|
+
import { Document } from "./Document.js";
|
|
5
|
+
import { Layout } from "./Layout.js";
|
|
6
|
+
export declare class RequestContext<Body extends LooseObject | undefined = LooseObject> {
|
|
7
|
+
readonly app: Application;
|
|
8
|
+
uri: string;
|
|
9
|
+
private readonly pageNotFoundCallback;
|
|
10
|
+
private readonly handler;
|
|
11
|
+
readonly request: IncomingMessage;
|
|
12
|
+
readonly response: ServerResponse;
|
|
13
|
+
args: URIArguments;
|
|
14
|
+
cookies: Record<string, string>;
|
|
15
|
+
body: Body;
|
|
16
|
+
bodyRaw?: Buffer;
|
|
17
|
+
files?: Record<string, RequestBodyRecordValue>;
|
|
18
|
+
data: RequestContextData;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
getArgs: PostedDataDecoded;
|
|
21
|
+
readonly timeStart: number;
|
|
22
|
+
private streamingData;
|
|
23
|
+
constructor(app: Application, request: IncomingMessage, response: ServerResponse, handler: RequestHandler | null, pageNotFoundCallback: RequestCallback<void | Document, LooseObject | undefined>);
|
|
24
|
+
private exec;
|
|
25
|
+
respondWith(data: any): Promise<void>;
|
|
26
|
+
private sendResponse;
|
|
27
|
+
createDocument(title: string, component: string, data?: LooseObject): Promise<Document>;
|
|
28
|
+
layoutDocument(layout: Layout, title: string, component: string, data?: LooseObject, attributes?: Record<string, string>): Promise<Document>;
|
|
29
|
+
show404(): Promise<void>;
|
|
30
|
+
private initGetArgs;
|
|
31
|
+
private parseCookies;
|
|
32
|
+
redirect(to: string, statusCode?: number): void;
|
|
33
|
+
private dataRaw;
|
|
34
|
+
private parseBody;
|
|
35
|
+
private parseBodyMultipart;
|
|
36
|
+
private multipartBodyFiles;
|
|
37
|
+
private extractURIArguments;
|
|
38
|
+
private handle;
|
|
39
|
+
isAjax(): boolean;
|
|
40
|
+
private error;
|
|
41
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import zlib from "node:zlib";
|
|
2
|
+
import { mergeDeep, queryStringDecode, queryStringDecodedSetValue } from "../Util.js";
|
|
3
|
+
import { Document } from "./Document.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { existsSync, readFileSync, ReadStream } from "node:fs";
|
|
6
|
+
export class RequestContext {
|
|
7
|
+
constructor(app, request, response, handler, pageNotFoundCallback) {
|
|
8
|
+
this.args = {};
|
|
9
|
+
this.cookies = {};
|
|
10
|
+
this.data = {};
|
|
11
|
+
this.getArgs = {};
|
|
12
|
+
this.streamingData = false;
|
|
13
|
+
this.timeStart = Date.now();
|
|
14
|
+
this.uri = request.url || '/';
|
|
15
|
+
this.app = app;
|
|
16
|
+
this.request = request;
|
|
17
|
+
this.response = response;
|
|
18
|
+
this.handler = handler;
|
|
19
|
+
this.body = undefined;
|
|
20
|
+
this.pageNotFoundCallback = pageNotFoundCallback;
|
|
21
|
+
this.exec();
|
|
22
|
+
}
|
|
23
|
+
async exec() {
|
|
24
|
+
this.initGetArgs();
|
|
25
|
+
this.parseCookies();
|
|
26
|
+
if (this.handler) {
|
|
27
|
+
try {
|
|
28
|
+
await this.parseBody();
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
this.error(e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
await this.handle();
|
|
35
|
+
}
|
|
36
|
+
async respondWith(data) {
|
|
37
|
+
if (typeof data === 'string' || Buffer.isBuffer(data)) {
|
|
38
|
+
this.sendResponse(data, 'text/plain; charset=utf-8');
|
|
39
|
+
}
|
|
40
|
+
else if (typeof data === 'number') {
|
|
41
|
+
this.sendResponse(data.toString(), 'text/plain; charset=utf-8');
|
|
42
|
+
}
|
|
43
|
+
else if (data instanceof Document) {
|
|
44
|
+
this.sendResponse(await data.toString(), 'text/html; charset=utf-8');
|
|
45
|
+
}
|
|
46
|
+
else if (data === undefined || data === null) {
|
|
47
|
+
this.sendResponse('', 'text/plain; charset=utf-8');
|
|
48
|
+
}
|
|
49
|
+
else if (data instanceof ReadStream) {
|
|
50
|
+
this.streamingData = true;
|
|
51
|
+
data.once('end', () => {
|
|
52
|
+
this.response.end();
|
|
53
|
+
});
|
|
54
|
+
data.pipe(this.response);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.sendResponse(JSON.stringify(data, null, 4), 'application/json; charset=utf-8');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
sendResponse(buffer, contentType) {
|
|
61
|
+
const mimeType = contentType.split(';')[0];
|
|
62
|
+
const gzipResponse = this.app.config.gzip.enabled &&
|
|
63
|
+
this.app.config.gzip.types.includes(mimeType) &&
|
|
64
|
+
this.request.headers['accept-encoding']?.includes('gzip') &&
|
|
65
|
+
buffer.length >= this.app.config.gzip.minSize;
|
|
66
|
+
if (!this.response.hasHeader('Content-Type')) {
|
|
67
|
+
this.response.setHeader('Content-Type', contentType);
|
|
68
|
+
}
|
|
69
|
+
if (typeof buffer === 'string') {
|
|
70
|
+
buffer = Buffer.from(buffer, 'utf-8');
|
|
71
|
+
}
|
|
72
|
+
if (gzipResponse) {
|
|
73
|
+
this.response.setHeader('Content-Encoding', 'gzip');
|
|
74
|
+
const compressed = zlib.gzipSync(buffer, {
|
|
75
|
+
level: this.app.config.gzip.compressionLevel
|
|
76
|
+
});
|
|
77
|
+
this.response.setHeader('Content-Length', compressed.length);
|
|
78
|
+
this.response.write(compressed);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.response.setHeader('Content-Length', buffer.length);
|
|
82
|
+
this.response.write(buffer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async createDocument(title, component, data) {
|
|
86
|
+
const doc = new Document(this.app, title, this);
|
|
87
|
+
await doc.loadComponent(component, data);
|
|
88
|
+
return doc;
|
|
89
|
+
}
|
|
90
|
+
async layoutDocument(layout, title, component, data, attributes) {
|
|
91
|
+
return await layout.document(this, title, component, data, attributes);
|
|
92
|
+
}
|
|
93
|
+
async show404() {
|
|
94
|
+
this.app.emit('pageNotFound', this);
|
|
95
|
+
this.response.statusCode = 404;
|
|
96
|
+
const res = await this.pageNotFoundCallback.apply(this.app, [this]);
|
|
97
|
+
if (res instanceof Document) {
|
|
98
|
+
await this.respondWith(res);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
initGetArgs() {
|
|
102
|
+
if (this.uri.indexOf('?') > -1) {
|
|
103
|
+
const uriParts = this.uri.split('?');
|
|
104
|
+
this.uri = uriParts[0];
|
|
105
|
+
this.getArgs = queryStringDecode(uriParts[1]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
parseCookies() {
|
|
109
|
+
this.cookies = this.app.cookies.parse(this.request);
|
|
110
|
+
}
|
|
111
|
+
redirect(to, statusCode = 302) {
|
|
112
|
+
this.response.setHeader('Location', to);
|
|
113
|
+
this.response.writeHead(statusCode);
|
|
114
|
+
}
|
|
115
|
+
dataRaw(request) {
|
|
116
|
+
const chunks = [];
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
request.on('data', (chunk) => {
|
|
119
|
+
chunks.push(chunk);
|
|
120
|
+
});
|
|
121
|
+
request.on('close', () => {
|
|
122
|
+
const size = chunks.reduce((prev, curr) => {
|
|
123
|
+
return prev + curr.length;
|
|
124
|
+
}, 0);
|
|
125
|
+
const data = Buffer.concat(chunks, size);
|
|
126
|
+
resolve(data);
|
|
127
|
+
});
|
|
128
|
+
request.on('error', (e) => {
|
|
129
|
+
reject(e);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async parseBody() {
|
|
134
|
+
if (this.request.headers['content-type']) {
|
|
135
|
+
this.bodyRaw = await this.dataRaw(this.request);
|
|
136
|
+
if (this.request.headers['content-type'].indexOf('urlencoded') > -1) {
|
|
137
|
+
const bodyRaw = this.bodyRaw.toString('utf-8');
|
|
138
|
+
try {
|
|
139
|
+
this.body = queryStringDecode(bodyRaw);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.error(`Error parsing urlencoded request body for request to ${this.request.url}, (${e.message}).
|
|
143
|
+
Raw data:
|
|
144
|
+
${bodyRaw}
|
|
145
|
+
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (this.request.headers['content-type'].indexOf('multipart/form-data') > -1) {
|
|
150
|
+
let boundary = /^multipart\/form-data; boundary=(.+)$/.exec(this.request.headers['content-type']);
|
|
151
|
+
if (boundary) {
|
|
152
|
+
boundary = `--${boundary[1]}`;
|
|
153
|
+
try {
|
|
154
|
+
this.body = this.parseBodyMultipart(this.bodyRaw.toString('utf-8'), boundary);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
console.error(`Error parsing multipart request body for request to ${this.request.url}, (${e.message}).
|
|
158
|
+
Raw data:
|
|
159
|
+
${this.bodyRaw.toString('utf-8')}
|
|
160
|
+
|
|
161
|
+
`);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
this.files = this.multipartBodyFiles(this.bodyRaw.toString('binary'), boundary);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
this.error(`Error parsing multipart request body files: (${e.message}).
|
|
168
|
+
Raw data:
|
|
169
|
+
${this.bodyRaw.toString('utf-8')}
|
|
170
|
+
|
|
171
|
+
`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (this.request.headers['content-type'].indexOf('application/json') > -1) {
|
|
176
|
+
try {
|
|
177
|
+
this.body = JSON.parse(this.bodyRaw.toString());
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
this.body = undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
parseBodyMultipart(bodyRaw, boundary) {
|
|
186
|
+
const pairsRaw = bodyRaw.split(boundary);
|
|
187
|
+
const pairs = pairsRaw.map((pair) => {
|
|
188
|
+
const parts = pair.split(/\r?\n\r?\n/, 2).filter((part) => { return part.length > 0; });
|
|
189
|
+
if (parts.length > 0) {
|
|
190
|
+
const header = parts[0];
|
|
191
|
+
const data = typeof parts[1] === 'string' ? parts[1].trim() : '';
|
|
192
|
+
const headerParts = /Content-Disposition: form-data; name="([^\r\n"]+)"/m.exec(header);
|
|
193
|
+
if (headerParts) {
|
|
194
|
+
return {
|
|
195
|
+
key: headerParts[1],
|
|
196
|
+
value: data
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
});
|
|
202
|
+
const urlEncoded = pairs.reduce((prev, curr) => {
|
|
203
|
+
if (curr !== null) {
|
|
204
|
+
prev.push(`${curr.key}=${encodeURIComponent(curr.value.replaceAll('&', '%26'))}`);
|
|
205
|
+
}
|
|
206
|
+
return prev;
|
|
207
|
+
}, []).join('&');
|
|
208
|
+
return queryStringDecode(urlEncoded);
|
|
209
|
+
}
|
|
210
|
+
multipartBodyFiles(bodyRaw, boundary) {
|
|
211
|
+
let files = {};
|
|
212
|
+
const pairsRaw = bodyRaw.split(boundary);
|
|
213
|
+
pairsRaw.map((pair) => {
|
|
214
|
+
const parts = /Content-Disposition: form-data; name="(.+?)"; filename="(.+?)"\r\nContent-Type: (.*)\r\n\r\n([\s\S]+)$/m.exec(pair);
|
|
215
|
+
if (parts) {
|
|
216
|
+
const file = {
|
|
217
|
+
data: Buffer.from(parts[4].substring(0, parts[4].length - 2).trim(), 'binary'),
|
|
218
|
+
fileName: parts[2],
|
|
219
|
+
type: parts[3]
|
|
220
|
+
};
|
|
221
|
+
files = mergeDeep(files, queryStringDecodedSetValue(parts[1], file));
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
});
|
|
225
|
+
return files;
|
|
226
|
+
}
|
|
227
|
+
extractURIArguments(uri, match) {
|
|
228
|
+
if (match instanceof RegExp) {
|
|
229
|
+
const matches = match.exec(uri);
|
|
230
|
+
if (matches) {
|
|
231
|
+
return {
|
|
232
|
+
matches
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
return {};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const uriArgs = {};
|
|
240
|
+
const segments = uri.split('/');
|
|
241
|
+
match.forEach((segmentPattern, i) => {
|
|
242
|
+
if (segmentPattern.name) {
|
|
243
|
+
uriArgs[segmentPattern.name] = segmentPattern.type === 'number' ? parseInt(segments[i]) : segments[i];
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return uriArgs;
|
|
247
|
+
}
|
|
248
|
+
async handle() {
|
|
249
|
+
if (this.handler !== null) {
|
|
250
|
+
if (!this.handler.staticAsset) {
|
|
251
|
+
const results = await this.app.emit('beforeRequestHandler', this);
|
|
252
|
+
if (results.includes(false)) {
|
|
253
|
+
this.response.end();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const URIArgs = this.extractURIArguments(this.uri, this.handler.match);
|
|
257
|
+
this.args = URIArgs;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const response = await this.handler.callback.apply(this.handler.scope, [this]);
|
|
261
|
+
if (!this.response.headersSent) {
|
|
262
|
+
await this.respondWith(response);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
console.log('Error executing request handler ', e, this.handler.callback.toString());
|
|
267
|
+
}
|
|
268
|
+
if (!this.handler.staticAsset) {
|
|
269
|
+
await this.app.emit('afterRequestHandler', this);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
let staticAsset = false;
|
|
274
|
+
if (this.app.config.url.isAsset(this.request.url || '')) {
|
|
275
|
+
const basePath = this.request.url?.startsWith('/assets/ts/') ? './' : '../';
|
|
276
|
+
const assetPath = path.resolve(basePath + this.request.url);
|
|
277
|
+
if (existsSync(assetPath)) {
|
|
278
|
+
await this.app.emit('beforeAssetAccess', this);
|
|
279
|
+
const extension = (this.request.url || '').split('.').pop();
|
|
280
|
+
let contentType = 'application/javascript';
|
|
281
|
+
if (extension) {
|
|
282
|
+
const typeByExtension = this.app.contentType(extension);
|
|
283
|
+
if (typeByExtension) {
|
|
284
|
+
contentType = typeByExtension;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
this.sendResponse(readFileSync(assetPath), contentType);
|
|
288
|
+
staticAsset = true;
|
|
289
|
+
await this.app.emit('afterAssetAccess', this);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!staticAsset) {
|
|
293
|
+
await this.show404();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!this.streamingData) {
|
|
297
|
+
this.response.end();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
isAjax() {
|
|
301
|
+
return this.request.headers['x-requested-with'] == 'xmlhttprequest';
|
|
302
|
+
}
|
|
303
|
+
error(e) {
|
|
304
|
+
const message = typeof e === 'string' ? e : e.message;
|
|
305
|
+
console.error(`Error in request to ${this.uri}: ${message}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -2,9 +2,9 @@ import { ClientComponent } from "../client/ClientComponent.js";
|
|
|
2
2
|
import { Net } from "../client/Net.js";
|
|
3
3
|
import { Application } from "../server/Application.js";
|
|
4
4
|
import { Component } from "../server/Component.js";
|
|
5
|
+
import { RequestContext } from "../server/RequestContext.js";
|
|
5
6
|
import { EventEmitterCallback } from "./eventEmitter.types.js";
|
|
6
7
|
import { KeysOfUnion, LooseObject } from './general.types.js';
|
|
7
|
-
import { RequestContext } from "./request.types.js";
|
|
8
8
|
export type ComponentEntry = {
|
|
9
9
|
name: string;
|
|
10
10
|
path: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { IncomingMessage, ServerResponse } from "http";
|
|
2
1
|
import { LooseObject } from './general.types.js';
|
|
3
2
|
import { symbolArrays } from "../Symbols.js";
|
|
3
|
+
import { RequestContext } from '../server/RequestContext.js';
|
|
4
4
|
export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
5
5
|
export type RequestCallback<R extends any, Body extends LooseObject | undefined> = (ctx: RequestContext<Body>) => Promise<R>;
|
|
6
6
|
export type RequestHandler = {
|
|
@@ -10,24 +10,6 @@ export type RequestHandler = {
|
|
|
10
10
|
scope: any;
|
|
11
11
|
staticAsset: boolean;
|
|
12
12
|
};
|
|
13
|
-
export type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
|
|
14
|
-
request: IncomingMessage;
|
|
15
|
-
response: ServerResponse;
|
|
16
|
-
args: URIArguments;
|
|
17
|
-
handler: null | RequestHandler;
|
|
18
|
-
cookies: Record<string, string>;
|
|
19
|
-
body: Body;
|
|
20
|
-
bodyRaw?: Buffer;
|
|
21
|
-
files?: Record<string, RequestBodyRecordValue>;
|
|
22
|
-
data: RequestContextData;
|
|
23
|
-
sessionId?: string;
|
|
24
|
-
isAjax: boolean;
|
|
25
|
-
getArgs: PostedDataDecoded;
|
|
26
|
-
timeStart: number;
|
|
27
|
-
respondWith: (data: any) => Promise<void>;
|
|
28
|
-
redirect: (to: string, statusCode?: number) => void;
|
|
29
|
-
show404: () => Promise<void>;
|
|
30
|
-
};
|
|
31
13
|
export type PostedDataDecoded = Record<string, string | boolean | Array<string | boolean | PostedDataDecoded> | Record<string, string | boolean | Array<string | boolean | PostedDataDecoded>> | Record<string, string | boolean | Array<string | boolean>>>;
|
|
32
14
|
export type RequestBodyRecordValue = string | Array<RequestBodyRecordValue> | {
|
|
33
15
|
[key: string]: RequestBodyRecordValue;
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"main": "build/index",
|
|
22
|
-
"version": "1.
|
|
22
|
+
"version": "1.5.0",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"develop": "tsc --watch",
|
|
25
25
|
"startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,hbs,css index.js",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"exports": {
|
|
51
51
|
"./Types": "./build/system/Types.js",
|
|
52
|
+
"./RequestContext": "./build/system/server/RequestContext.js",
|
|
52
53
|
"./Symbols": "./build/system/Symbols.js",
|
|
53
54
|
"./Util": "./build/system/Util.js",
|
|
54
55
|
"./Application": "./build/system/server/Application.js",
|