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 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
- type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
262
- request: IncomingMessage,
263
- response: ServerResponse,
264
- args: URIArguments,
265
- handler: null|RequestHandler,
268
+ class RequestContext<Body extends LooseObject | undefined = LooseObject> = {
269
+ app: Application;
270
+ uri: string;
271
+
272
+ request: IncomingMessage;
273
+ response: ServerResponse;
266
274
 
267
- cookies: Record<string, string>,
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 received and it equals 'xmlhttprequest'
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
- // URL GET arguments
290
- getArgs: PostedDataDecoded,
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 url, with given statusCode
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, RequestContext } from 'structured-fw/Types';
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, title: string, componentName: string, data?: LooseObject, attributes?: Record<string, string>): Promise<Document>;
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, PostedDataDecoded, RequestBodyFile, RequestCallback } from "../types/request.types.js";
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 { mergeDeep, queryStringDecode, queryStringDecodedSetValue } from "../Util.js";
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 { Buffer } from "node:buffer";
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
- const context = {
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.4.0",
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",