structured-fw 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/Config.ts +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +332 -0
  4. package/app/Types.ts +1 -0
  5. package/app/models/README.md +9 -0
  6. package/app/routes/README.md +19 -0
  7. package/app/views/README.md +1 -0
  8. package/app/views/layout.html +1 -0
  9. package/bin/structured +114 -0
  10. package/build/Config.d.ts +2 -0
  11. package/build/Config.js +31 -0
  12. package/build/app/Types.d.ts +1 -0
  13. package/build/app/Types.js +1 -0
  14. package/build/app/models/Users.d.ts +0 -0
  15. package/build/app/models/Users.js +1 -0
  16. package/build/app/routes/Auth.d.ts +0 -0
  17. package/build/app/routes/Auth.js +1 -0
  18. package/build/app/routes/Test.d.ts +2 -0
  19. package/build/app/routes/Test.js +101 -0
  20. package/build/app/routes/Todo.d.ts +0 -0
  21. package/build/app/routes/Todo.js +1 -0
  22. package/build/app/routes/Upload.d.ts +0 -0
  23. package/build/app/routes/Upload.js +1 -0
  24. package/build/app/routes/Validation.d.ts +2 -0
  25. package/build/app/routes/Validation.js +34 -0
  26. package/build/app/views/components/ClientImport/ClientImport.client.d.ts +2 -0
  27. package/build/app/views/components/ClientImport/ClientImport.client.js +4 -0
  28. package/build/app/views/components/ClientImport/Export.d.ts +1 -0
  29. package/build/app/views/components/ClientImport/Export.js +1 -0
  30. package/build/app/views/components/Conditionals/Conditionals.client.d.ts +2 -0
  31. package/build/app/views/components/Conditionals/Conditionals.client.js +43 -0
  32. package/build/app/views/components/FormTest/FormTestNested/FormTestNested.d.ts +8 -0
  33. package/build/app/views/components/FormTest/FormTestNested/FormTestNested.js +7 -0
  34. package/build/app/views/components/ModelsTest/ModelsTest.client.d.ts +2 -0
  35. package/build/app/views/components/ModelsTest/ModelsTest.client.js +5 -0
  36. package/build/app/views/components/MultipartForm/MultipartForm.client.d.ts +0 -0
  37. package/build/app/views/components/MultipartForm/MultipartForm.client.js +1 -0
  38. package/build/app/views/components/PassObject/PassObject.d.ts +10 -0
  39. package/build/app/views/components/PassObject/PassObject.js +10 -0
  40. package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.d.ts +6 -0
  41. package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.js +6 -0
  42. package/build/app/views/components/RedrawAbort/RedrawAbort.client.d.ts +2 -0
  43. package/build/app/views/components/RedrawAbort/RedrawAbort.client.js +6 -0
  44. package/build/app/views/components/RedrawAbort/RedrawAbort.d.ts +8 -0
  45. package/build/app/views/components/RedrawAbort/RedrawAbort.js +8 -0
  46. package/build/app/views/components/ServerSideContext/ServerSideContext.d.ts +7 -0
  47. package/build/app/views/components/ServerSideContext/ServerSideContext.js +10 -0
  48. package/build/assets/ts/Export.d.ts +1 -0
  49. package/build/assets/ts/Export.js +1 -0
  50. package/build/index.d.ts +1 -0
  51. package/build/index.js +3 -0
  52. package/build/system/Helpers.d.ts +3 -0
  53. package/build/system/Helpers.js +72 -0
  54. package/build/system/Symbols.d.ts +3 -0
  55. package/build/system/Symbols.js +3 -0
  56. package/build/system/Types.d.ts +171 -0
  57. package/build/system/Types.js +1 -0
  58. package/build/system/Util.d.ts +20 -0
  59. package/build/system/Util.js +336 -0
  60. package/build/system/client/App.d.ts +7 -0
  61. package/build/system/client/App.js +8 -0
  62. package/build/system/client/Client.d.ts +6 -0
  63. package/build/system/client/Client.js +9 -0
  64. package/build/system/client/ClientComponent.d.ts +68 -0
  65. package/build/system/client/ClientComponent.js +734 -0
  66. package/build/system/client/DataStore.d.ts +22 -0
  67. package/build/system/client/DataStore.js +64 -0
  68. package/build/system/client/DataStoreView.d.ts +19 -0
  69. package/build/system/client/DataStoreView.js +56 -0
  70. package/build/system/client/EventEmitter.d.ts +7 -0
  71. package/build/system/client/EventEmitter.js +31 -0
  72. package/build/system/client/Net.d.ts +13 -0
  73. package/build/system/client/Net.js +39 -0
  74. package/build/system/client/NetRequest.d.ts +13 -0
  75. package/build/system/client/NetRequest.js +45 -0
  76. package/build/system/server/Application.d.ts +31 -0
  77. package/build/system/server/Application.js +171 -0
  78. package/build/system/server/Component.d.ts +27 -0
  79. package/build/system/server/Component.js +249 -0
  80. package/build/system/server/Components.d.ts +12 -0
  81. package/build/system/server/Components.js +77 -0
  82. package/build/system/server/Cookies.d.ts +6 -0
  83. package/build/system/server/Cookies.js +19 -0
  84. package/build/system/server/Document.d.ts +24 -0
  85. package/build/system/server/Document.js +107 -0
  86. package/build/system/server/DocumentHead.d.ts +32 -0
  87. package/build/system/server/DocumentHead.js +118 -0
  88. package/build/system/server/FormValidation.d.ts +16 -0
  89. package/build/system/server/FormValidation.js +197 -0
  90. package/build/system/server/Handlebars.d.ts +11 -0
  91. package/build/system/server/Handlebars.js +34 -0
  92. package/build/system/server/Request.d.ts +21 -0
  93. package/build/system/server/Request.js +356 -0
  94. package/build/system/server/Session.d.ts +23 -0
  95. package/build/system/server/Session.js +114 -0
  96. package/build/system/server/dom/DOMFragment.d.ts +4 -0
  97. package/build/system/server/dom/DOMFragment.js +6 -0
  98. package/build/system/server/dom/DOMNode.d.ts +31 -0
  99. package/build/system/server/dom/DOMNode.js +110 -0
  100. package/build/system/server/dom/HTMLParser.d.ts +21 -0
  101. package/build/system/server/dom/HTMLParser.js +204 -0
  102. package/index.ts +4 -0
  103. package/package.json +31 -0
  104. package/system/Helpers.ts +97 -0
  105. package/system/Symbols.ts +6 -0
  106. package/system/Types.ts +234 -0
  107. package/system/Util.ts +488 -0
  108. package/system/client/App.ts +11 -0
  109. package/system/client/Client.ts +9 -0
  110. package/system/client/ClientComponent.ts +1117 -0
  111. package/system/client/DataStore.ts +101 -0
  112. package/system/client/DataStoreView.ts +82 -0
  113. package/system/client/EventEmitter.ts +38 -0
  114. package/system/client/Net.ts +58 -0
  115. package/system/client/NetRequest.ts +64 -0
  116. package/system/server/Application.ts +230 -0
  117. package/system/server/Component.ts +404 -0
  118. package/system/server/Components.ts +111 -0
  119. package/system/server/Cookies.ts +29 -0
  120. package/system/server/Document.ts +163 -0
  121. package/system/server/DocumentHead.ts +150 -0
  122. package/system/server/FormValidation.ts +231 -0
  123. package/system/server/Handlebars.ts +51 -0
  124. package/system/server/Request.ts +497 -0
  125. package/system/server/Session.ts +151 -0
  126. package/system/server/dom/DOMFragment.ts +7 -0
  127. package/system/server/dom/DOMNode.ts +140 -0
  128. package/system/server/dom/HTMLParser.ts +238 -0
  129. package/tsconfig.json +35 -0
@@ -0,0 +1,497 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import { PostedDataDecoded, RequestBodyFile, RequestCallback, RequestContext, RequestHandler, RequestMethod, URIArguments, URISegmentPattern } from "../Types.js";
3
+ import { mergeDeep, queryStringDecode, queryStringDecodedSetValue } from "../Util.js";
4
+ import { RequestContextData } from "../../app/Types.js";
5
+ import { Application } from "./Application.js";
6
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { Buffer } from "node:buffer";
9
+ import { Document } from "./Document.js";
10
+
11
+ export class Request {
12
+
13
+ private app: Application;
14
+
15
+ constructor(app: Application) {
16
+ this.app = app;
17
+ }
18
+
19
+ pageNotFoundCallback: RequestCallback = async ({ response }) => {
20
+ response.statusCode = 404;
21
+ response.write('Page not found');
22
+ response.end();
23
+ };
24
+
25
+ // registered request handlers
26
+ private readonly handlers: Array<RequestHandler> = [];
27
+
28
+ // registers a request handler for given request method(s) + pattern(s)
29
+ // when a request is made that matches the request method(s) and pattern(s) callback is executed
30
+ // pattern can have matches in it which will later populate ctx.args, eg. /users/(id:num) or /example/(argName)
31
+ // callback.this will be the scope if scope is provided, otherwise scope is the Application instance
32
+ // if pattern is given as array, one request handler will be created for each element of the array
33
+ // if isStaticAsset = true, (before/after)RequestHandler event not emitted, body and GET args not parsed
34
+ public on(
35
+ methods: RequestMethod|Array<RequestMethod>,
36
+ pattern: string|RegExp|Array<string|RegExp>,
37
+ callback: RequestCallback,
38
+ scope?: any,
39
+ isStaticAsset: boolean = false
40
+ ): void {
41
+
42
+ if (! (methods instanceof Array)) {
43
+ methods = [methods];
44
+ }
45
+
46
+ if (scope === undefined) {
47
+ scope = this.app;
48
+ }
49
+
50
+ // if pattern was given as an array, call addRequestHandler with each item in array
51
+ if (pattern instanceof Array) {
52
+ pattern.forEach((p) => {
53
+ this.on(methods, p, callback, scope);
54
+ });
55
+ return;
56
+ }
57
+
58
+ const match = ((typeof pattern === 'string' ? this.patternToSegments(pattern) : pattern) as RegExp|Array<URISegmentPattern>);
59
+
60
+ const handler: RequestHandler = {
61
+ match,
62
+ methods,
63
+ callback,
64
+ scope,
65
+ staticAsset: isStaticAsset
66
+ }
67
+
68
+ this.handlers.push(handler);
69
+
70
+ // sort request handlers so that non-regexp uri's come first, to speed up search
71
+ this.handlers.sort((a, b) => {
72
+ const valA: number = a.match instanceof RegExp ? 1 : 0;
73
+ const valB: number = b.match instanceof RegExp ? 1 : 0;
74
+
75
+ return valA - valB;
76
+ });
77
+ }
78
+
79
+ // if there is a handler registered for the given URI, returns the handler, null otherwise
80
+ private getHandler(uri: string, method: RequestMethod): null|RequestHandler {
81
+ const segments = uri.split('/');
82
+
83
+ // narrowing down the possible handlers, first iteration
84
+ // any that have the correct number of segments and RegExp's
85
+ let possible = this.handlers.filter((handler) => {
86
+ return handler.methods.includes(method) && (handler.match instanceof RegExp || handler.match.length === segments.length);
87
+ });
88
+
89
+ // check uri, segment-by-segment
90
+ for (let i = 0; i < segments.length; i++) {
91
+ possible = possible.filter((handler) => {
92
+ if (handler.match instanceof RegExp) {
93
+ // match is a RegExp, keep if matches the uri
94
+ return handler.match.test(uri);
95
+ } else {
96
+ // handler.match is an array, check against current segment patter
97
+ // it can be a string or RegExp
98
+ const pattern = handler.match[i].pattern;
99
+ if (typeof pattern === 'string') {
100
+ return pattern === segments[i];
101
+ }
102
+
103
+ // current segment pattern is a RegExp
104
+ return pattern.test(segments[i]);
105
+ }
106
+ });
107
+ }
108
+
109
+ // no possible request handlers registered
110
+ if (possible.length === 0) {
111
+ return null;
112
+ }
113
+
114
+ // multiple requests handlers match
115
+ if (possible.length > 1) {
116
+ console.warn(`Multiple request handlers for ${uri}`);
117
+ }
118
+
119
+ // prefer constant matches over RegExp
120
+ possible.sort((a, b) => {
121
+ if (a.match instanceof Array && b.match instanceof Array) {
122
+ // a an b are of same length
123
+ for (let i = a.match.length - 1; i > -1; i--) {
124
+ const aVal = a.match[i].pattern instanceof RegExp ? 1 : 0;
125
+ const bVal = b.match[i].pattern instanceof RegExp ? 1 : 0;
126
+ if (aVal != bVal) {
127
+ return aVal - bVal;
128
+ }
129
+ }
130
+ return 0;
131
+ }
132
+
133
+ const aVal = a.match instanceof RegExp ? 1 : 0;
134
+ const bVal = b.match instanceof RegExp ? 1 : 0;
135
+ return aVal - bVal;
136
+ });
137
+
138
+ // return the best match
139
+ return possible[0];
140
+ }
141
+
142
+ // handle a request
143
+ // checks whether there is a registered handler for the URL
144
+ // if not then it tries to serve a static asset if path is allowd by Config.assets.allow
145
+ // if it's not allowed or the asset does not exits, 404 callback is executed
146
+ public async handle(request: IncomingMessage, response: ServerResponse): Promise<void> {
147
+ const requestMethod = request.method as RequestMethod;
148
+
149
+ let uri = request.url || '/';
150
+
151
+ if (this.app.config.url.removeTrailingSlash && uri.length > 1 && uri.endsWith('/')) {
152
+ uri = uri.substring(0, uri.length - 1);
153
+ }
154
+
155
+ // extract any GET args from the URL-provided query string eg. /page?key=val
156
+ // once extracted, remove the query string part of the URL
157
+ let getArgs = {};
158
+ if (uri.indexOf('?') > -1) {
159
+ const uriParts = uri.split('?');
160
+ uri = uriParts[0];
161
+ getArgs = queryStringDecode(uriParts[1]);
162
+ }
163
+
164
+ // get the best matching request handler
165
+ const handler = this.getHandler(uri, requestMethod);
166
+
167
+ const context: RequestContext = {
168
+ request,
169
+ response,
170
+ handler,
171
+ args: {},
172
+ // RequestContext.data is place for user defined data
173
+ // it is initialized as an empty object here and
174
+ // potentially falsely declared as RequestContextData
175
+ // user will fill this out, usually on beforeRequestHandler
176
+ data: {} as RequestContextData,
177
+ getArgs,
178
+ cookies: this.app.cookies.parse(request),
179
+ isAjax : request.headers['x-requested-with'] == 'xmlhttprequest',
180
+ respondWith: function (data: any) {
181
+ if (typeof data === 'string' || Buffer.isBuffer(data)) {
182
+ response.write(data);
183
+ } else if (typeof data === 'number') {
184
+ response.write(data.toString());
185
+ } else if (data instanceof Document) {
186
+ response.setHeader('Content-Type', 'text/html');
187
+ response.write(data.toString());
188
+ } else if (data === undefined || data === null) {
189
+ response.write('');
190
+ } else {
191
+ response.setHeader('Content-Type', 'application/json');
192
+ response.write(JSON.stringify(data, null, 4));
193
+ }
194
+ },
195
+ redirect: (to: string, statusCode: number = 302) => {
196
+ this.redirect(response, to, statusCode);
197
+ },
198
+ show404: async() => {
199
+ await this.pageNotFoundCallback.apply(this.app, [context]);
200
+ this.app.emit('pageNotFound', context);
201
+ }
202
+ }
203
+
204
+
205
+ if (handler !== null) {
206
+ // handler exists
207
+ if (! handler.staticAsset) {
208
+ // run beforeRequestHandler callbacks
209
+ const results = await this.app.emit('beforeRequestHandler', context);
210
+
211
+ // if any of the beforeRequestHandler callbacks returned false, end the request here
212
+ // this provides a way for the developer to prevent the request handler from being executed
213
+ // which is useful in some cases, for example:
214
+ // beforeRequestHandler checks if user is logged in, if not redirects to login
215
+ // and returns false to prevent unauthorized access to user-only page
216
+ if (results.includes(false)) {
217
+ context.response.end();
218
+ return;
219
+ }
220
+
221
+ try {
222
+ // parse request body, this will populate ctx.bodyRaw and if possible ctx.body
223
+ await this.parseBody(context);
224
+ } catch(e) {
225
+ console.error(`Error parsing request body: ${e.message}`);
226
+ }
227
+
228
+ // extract URI arguments, if pattern included capture groups, those will be included
229
+ // for example pattern /users/(userId:num) -> { userId: number }
230
+ const URIArgs = this.extractURIArguments(uri, handler.match);
231
+ context.args = URIArgs;
232
+ }
233
+
234
+
235
+ // run the request handler callback
236
+ try {
237
+ const response = await handler.callback.apply(handler.scope, [context]);
238
+ if (! context.response.headersSent) {
239
+ // if the response was not sent from the request handler
240
+ // respond with whatever the handler has returned
241
+ context.respondWith(response);
242
+ }
243
+ } catch(e) {
244
+ console.log('Error executing request handler ', e, handler.callback.toString());
245
+ }
246
+
247
+ if (! handler.staticAsset) {
248
+ await this.app.emit('afterRequestHandler', context);
249
+ }
250
+ } else {
251
+ // handler not found, check if a static asset is requested
252
+ let staticAsset = false;
253
+
254
+ if (this.app.config.url.isAsset(context.request.url || '')) {
255
+ // static asset
256
+ // unless accessing /assets/ts/* go directory up to get out of build
257
+ const basePath = context.request.url?.startsWith('/assets/ts/') ? './' : '../';
258
+ const assetPath = path.resolve(basePath + context.request.url);
259
+ if (existsSync(assetPath)) {
260
+ await this.app.emit('beforeAssetAccess', context);
261
+ const extension = (context.request.url || '').split('.').pop();
262
+ if (extension) {
263
+ const contentType = this.app.contentType(extension);
264
+ if (contentType) {
265
+ response.setHeader('Content-Type', contentType);
266
+ }
267
+ }
268
+ response.write(readFileSync(assetPath));
269
+ staticAsset = true;
270
+ await this.app.emit('afterAssetAccess', context);
271
+ }
272
+ }
273
+
274
+ if (! staticAsset) {
275
+ // no request handler found nor a static asset - 404
276
+ await context.show404();
277
+ }
278
+
279
+ }
280
+
281
+ // end the response
282
+ response.end();
283
+ }
284
+
285
+ // extract variables from the given URI, using provided match which is defined by current request handler
286
+ // hence this only gets executed for requests that have a registered handler
287
+ private extractURIArguments(uri: string, match: Array<URISegmentPattern>|RegExp): URIArguments {
288
+ if (match instanceof RegExp) {
289
+ const matches = match.exec(uri);
290
+ if (matches) {
291
+ return {
292
+ matches
293
+ };
294
+ } else {
295
+ return {};
296
+ }
297
+ }
298
+
299
+ const uriArgs:URIArguments = {};
300
+
301
+ const segments = uri.split('/');
302
+
303
+ match.forEach((segmentPattern, i) => {
304
+ if (segmentPattern.name) {
305
+ uriArgs[segmentPattern.name] = segmentPattern.type === 'number' ? parseInt(segments[i]) : segments[i];
306
+ }
307
+ });
308
+
309
+ return uriArgs;
310
+ }
311
+
312
+ // allows easy access to URI segments
313
+ // (varname) - match any value
314
+ // (varname:num) - match a number
315
+ private patternToSegments(pattern: string): Array<URISegmentPattern> {
316
+ const segments: Array<URISegmentPattern> = [];
317
+
318
+ const segmentsIn = pattern.split('/');
319
+
320
+ segmentsIn.forEach((segmentIn) => {
321
+ const named = /^\([^\/]+\)$/.test(segmentIn);
322
+ const segmentPattern: URISegmentPattern = {
323
+ pattern: segmentIn,
324
+ type: 'string'
325
+ }
326
+ if (named) {
327
+ const nameParts = /^\(([^\/:\)]+)/.exec(segmentIn);
328
+ const isNumber = /:num\)$/.test(segmentIn);
329
+ if (nameParts) {
330
+ segmentPattern.name = nameParts[1];
331
+ if (isNumber) {
332
+ segmentPattern.pattern = /^(\d+)$/;
333
+ segmentPattern.type = 'number';
334
+ } else {
335
+ segmentPattern.pattern = /^([^\/]+)$/;
336
+ segmentPattern.type = 'string';
337
+ }
338
+ } else {
339
+ console.warn(`Invalid URI segment pattern ${segmentIn} in URI ${pattern}`);
340
+ }
341
+ }
342
+ segments.push(segmentPattern);
343
+ });
344
+
345
+ return segments;
346
+ }
347
+
348
+ // parse raw request body
349
+ // if there is a parser for received Content-Type
350
+ // then ctx.body is populated with data: URIArgs
351
+ private async parseBody(ctx: Omit<RequestContext, 'data'>): Promise<void> {
352
+ if (ctx.request.headers['content-type']) {
353
+
354
+ ctx.bodyRaw = await this.dataRaw(ctx.request);
355
+
356
+ if (ctx.request.headers['content-type'].indexOf('urlencoded') > -1) {
357
+ // application/x-www-form-urlencoded
358
+ ctx.body = queryStringDecode(ctx.bodyRaw.toString('utf-8'));
359
+ } else if (ctx.request.headers['content-type'].indexOf('multipart/form-data') > -1) {
360
+ // multipart/form-data
361
+ let boundary: RegExpExecArray|null|string = /^multipart\/form-data; boundary=(.+)$/.exec(ctx.request.headers['content-type']);
362
+ if (boundary) {
363
+ boundary = `--${boundary[1]}`;
364
+ ctx.body = Request.parseBodyMultipart(ctx.bodyRaw.toString('utf-8'), boundary);
365
+ ctx.files = Request.multipartBodyFiles(ctx.bodyRaw.toString('binary'), boundary);
366
+ }
367
+ } else if (ctx.request.headers['content-type'].indexOf('application/json') > -1) {
368
+ // application/json
369
+ try {
370
+ ctx.body = JSON.parse(ctx.bodyRaw.toString());
371
+ } catch (e) {
372
+ // failed to parse the body
373
+ ctx.body = undefined;
374
+ }
375
+ }
376
+ }
377
+ return;
378
+ }
379
+
380
+ // returns the raw request data (eg. POST'ed data)
381
+ private dataRaw(request: IncomingMessage): Promise<Buffer> {
382
+ const chunks: Array<Buffer> = [];
383
+
384
+ return new Promise((resolve, reject) => {
385
+ request.on('data', (chunk) => {
386
+ chunks.push(chunk);
387
+ });
388
+
389
+ request.on('close', () => {
390
+ // calculate the total size of all chunks
391
+ const size = chunks.reduce((prev, curr) => {
392
+ return prev + curr.length;
393
+ }, 0);
394
+
395
+ // combine the chunks to form final data
396
+ const data = Buffer.concat(chunks, size);
397
+
398
+ resolve(data);
399
+ });
400
+
401
+ request.on('error', (e) => {
402
+ reject(e);
403
+ });
404
+ });
405
+ }
406
+
407
+ // send the headers to redirect the client, 302 redirect by default
408
+ // should be called before any output (before any res.write)
409
+ public redirect(response: ServerResponse, to: string, statusCode: number = 302): void {
410
+ response.setHeader('Location', to);
411
+ response.writeHead(statusCode);
412
+ }
413
+
414
+ // load request handlers from given directory recursively
415
+ // if directory is omitted, loads from conf.routes.path
416
+ public async loadHandlers(basePath?: string): Promise<void> {
417
+ let routesPath:string;
418
+ if (basePath) {
419
+ routesPath = basePath;
420
+ } else {
421
+ routesPath = path.resolve((this.app.config.runtime === 'Node.js' ? '../build/' : './') + this.app.config.routes.path);
422
+ }
423
+ const files = readdirSync(routesPath);
424
+
425
+ for (let i = 0; i < files.length; i++) {
426
+ const file = files[i];
427
+ if (! (file.endsWith('.js') || file.endsWith('.ts'))) {
428
+ continue;
429
+ }
430
+ const filePath = path.resolve(routesPath + '/' + file);
431
+ const isDirectory = statSync(filePath).isDirectory();
432
+ if (isDirectory) {
433
+ await this.loadHandlers(filePath);
434
+ } else {
435
+ const fn = (await import('file:///' + filePath)).default;
436
+ if (typeof fn === 'function') {
437
+ fn(this.app);
438
+ }
439
+ }
440
+ }
441
+
442
+ return;
443
+ }
444
+
445
+ public static queryStringDecode(queryString: string, initialValue: PostedDataDecoded = {}, trimValues: boolean = true): PostedDataDecoded {
446
+ return queryStringDecode(queryString, initialValue, trimValues);
447
+ }
448
+
449
+ // process raw multipart/form-data body into an object
450
+ // boundary has to be provided as second argument
451
+ public static parseBodyMultipart(bodyRaw: string, boundary: string): PostedDataDecoded {
452
+ const pairsRaw = bodyRaw.split(boundary);
453
+ const pairs = pairsRaw.map((pair) => {
454
+ const parts = /Content-Disposition: form-data; name="([^\r\n"]+)"\r?\n\r?\n([^$]+)/m.exec(pair);
455
+ if (parts) {
456
+ return {
457
+ key: parts[1],
458
+ value: parts[2]
459
+ }
460
+ }
461
+ return null;
462
+ });
463
+
464
+ // convert data to query string
465
+ const urlEncoded = pairs.reduce((prev, curr) => {
466
+ if (curr !== null) {
467
+ prev.push(`${curr.key}=${encodeURIComponent(curr.value.replaceAll('&', '%26'))}`);
468
+ }
469
+ return prev;
470
+ }, [] as Array<string>).join('&');
471
+
472
+ return queryStringDecode(urlEncoded);
473
+ }
474
+
475
+ public static multipartBodyFiles(bodyRaw: string, boundary: string): Record<string, RequestBodyFile> {
476
+ let files: Record<string, RequestBodyFile> = {}
477
+ const pairsRaw = bodyRaw.split(boundary);
478
+ pairsRaw.map((pair) => {
479
+ const parts = /Content-Disposition: form-data; name="(.+?)"; filename="(.+?)"\r\nContent-Type: (.*)\r\n\r\n([\s\S]+)$/m.exec(pair);
480
+ if (parts) {
481
+ const file: RequestBodyFile = {
482
+ data: Buffer.from(parts[4].substring(0, parts[4].length - 2).trim(), 'binary'),
483
+ fileName: parts[2],
484
+ type: parts[3]
485
+ }
486
+ // we can't just set the file as files[parts[1]] = file
487
+ // that would work if parts[1] is a simple key without "[.*]" in it
488
+ // but in reality key will often be an object or an array
489
+ // so we need to recursively create the object and fill it with file
490
+ // then merge that result with resulting files object
491
+ files = mergeDeep(files, queryStringDecodedSetValue(parts[1], file));
492
+ }
493
+ return null;
494
+ })
495
+ return files;
496
+ }
497
+ }
@@ -0,0 +1,151 @@
1
+ import { randomString } from '../Util.js';
2
+ import { LooseObject, RequestContext, SessionEntry } from '../Types.js';
3
+ import { Application } from './Application.js';
4
+
5
+ export class Session {
6
+
7
+ application: Application;
8
+ enabled: boolean = false;
9
+
10
+ sessions: {
11
+ [key: string] : SessionEntry
12
+ } = {};
13
+
14
+ constructor(app: Application) {
15
+ this.application = app;
16
+
17
+ // bind the event listener to beforeRequestHandler
18
+ this.application.on('beforeRequestHandler', async (ctx: RequestContext) => {
19
+ if (this.enabled) {
20
+ const sessionCookie = ctx.cookies[this.application.config.session.cookieName];
21
+
22
+ const invalidSessionId = sessionCookie && ! this.sessions[sessionCookie];
23
+
24
+ if (! sessionCookie || invalidSessionId) {
25
+ // user has no started session, initialize session
26
+ this.sessionInit(ctx);
27
+ } else {
28
+ ctx.sessionId = sessionCookie;
29
+ if (ctx.sessionId) {
30
+ // refresh cookie
31
+ this.application.cookies.set(
32
+ ctx.response,
33
+ this.application.config.session.cookieName,
34
+ ctx.sessionId,
35
+ this.application.config.session.durationSeconds
36
+ );
37
+ this.sessions[ctx.sessionId].lastRequest = new Date().getTime();
38
+ }
39
+ }
40
+ }
41
+ });
42
+
43
+ // start garbage collecting
44
+ this.garbageCollect();
45
+ }
46
+
47
+ public start(): void {
48
+ this.enabled = true;
49
+ }
50
+
51
+ public stop(): void {
52
+ this.enabled = false;
53
+ }
54
+
55
+ private sessionInit(ctx: RequestContext): void {
56
+ ctx.sessionId = this.generateId();
57
+ this.application.cookies.set(ctx.response, this.application.config.session.cookieName, ctx.sessionId, this.application.config.session.durationSeconds);
58
+
59
+ // create and store session entry
60
+ const sessionEntry: SessionEntry = {
61
+ sessionId: ctx.sessionId,
62
+ lastRequest: new Date().getTime(),
63
+ data: {}
64
+ }
65
+
66
+ this.sessions[ctx.sessionId] = sessionEntry;
67
+ }
68
+
69
+ private generateId(): string {
70
+ return randomString(this.application.config.session.keyLength);
71
+ }
72
+
73
+ // remove expired session entries
74
+ private garbageCollect(): void {
75
+ const time = new Date().getTime();
76
+ const sessDurationMilliseconds = this.application.config.session.garbageCollectAfterSeconds * 1000;
77
+
78
+ for (const sessionId in this.sessions) {
79
+ const sess = this.sessions[sessionId];
80
+ if (time - sess.lastRequest > sessDurationMilliseconds) {
81
+ // expired session
82
+ delete this.sessions[sessionId];
83
+ }
84
+ }
85
+
86
+ // resume garbage collection after configured interval
87
+ setTimeout(() => {
88
+ this.garbageCollect();
89
+ }, this.application.config.session.garbageCollectIntervalSeconds * 1000);
90
+ }
91
+
92
+ // reason for sessionId being allowed as undefined|null is that RequestContext.sessionId can be undefined
93
+ public setValue(sessionId: string|undefined, key: string, value: any): void {
94
+ if (sessionId === undefined) {return;}
95
+ if (this.sessions[sessionId]) {
96
+ const session = this.sessions[sessionId];
97
+ session.data[key] = value;
98
+ }
99
+ }
100
+
101
+ // value or null if session does not exist
102
+ public getValue<T>(sessionId: string|undefined, key: string): T|null {
103
+ if (sessionId === undefined) {return null;}
104
+ if (this.sessions[sessionId]) {
105
+ const session = this.sessions[sessionId];
106
+ return typeof session.data[key] !== 'undefined' ? session.data[key] : null;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ // return value and clear it from session
112
+ public getClear<T>(sessionId: string|undefined, key: string): T | null {
113
+ const val = this.getValue<T>(sessionId, key);
114
+ this.removeValue(sessionId, key);
115
+ return val;
116
+ }
117
+
118
+ public removeValue(sessionId: string|undefined, key: string): void {
119
+ if (sessionId === undefined) {return;}
120
+ if (this.sessions[sessionId] && this.sessions[sessionId].data[key]) {
121
+ delete this.sessions[sessionId].data[key];
122
+ }
123
+ }
124
+
125
+ // remove all stored data for the given session
126
+ public clear(sessionId: string|undefined): void {
127
+ if (sessionId === undefined) {return;}
128
+ if (this.sessions[sessionId]) {
129
+ this.sessions[sessionId].data = {};
130
+ }
131
+ }
132
+
133
+ // extract given keys from session and return them as an object
134
+ // key in keys can be a string in which case the key will remain the same in returned object
135
+ // or it can be an object { keyInSession : keyInReturnedData } in which case key in returned data will be keyInReturnedData
136
+ public extract(sessionId: string|undefined, keys: Array<string|{ [keyInSession: string] : string }>): LooseObject {
137
+ if (sessionId === undefined) {return {};}
138
+ const data: LooseObject = {};
139
+ keys.forEach((key) => {
140
+ if (typeof key === 'string') {
141
+ data[key] = this.getValue(sessionId, key);
142
+ } else {
143
+ const keyInSession = Object.keys(key)[0];
144
+ const keyReturned = key[keyInSession];
145
+ data[keyReturned] = this.getValue(sessionId, keyInSession);
146
+ }
147
+ });
148
+ return data;
149
+ }
150
+
151
+ }
@@ -0,0 +1,7 @@
1
+ import { DOMNode } from "./DOMNode.js";
2
+
3
+ export class DOMFragment extends DOMNode {
4
+ constructor() {
5
+ super('body');
6
+ }
7
+ }