structured-fw 0.7.91 → 0.8.1

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/Config.ts CHANGED
@@ -34,8 +34,7 @@ export const config: StructuredConfig = {
34
34
  cookieName: 'session',
35
35
  keyLength: 24,
36
36
  durationSeconds: 60 * 60,
37
- garbageCollectIntervalSeconds: 60,
38
- garbageCollectAfterSeconds: 500
37
+ garbageCollectIntervalSeconds: 60
39
38
  },
40
39
  http: {
41
40
  port: 9191,
package/README.md CHANGED
@@ -98,7 +98,7 @@ new Application(config);
98
98
 
99
99
  ### Properties
100
100
  - `cookies` - Instance of Cookies, allows you to set a cookie
101
- - `session` - Instance of Session, utilities to manage sessions and data
101
+ - [`session`](#session) - Instance of Session, utilities to manage sessions and data
102
102
  - `request` - Instance of Request, you will use this to add routes, but usually not directly by accessing Application.request, more on that in [routes](#route) section
103
103
  - `handlebars` - Instance of Handlebars (wrapper around Handlebars templating engine)
104
104
  - `components` - Instance of Components, this is the components registry, you should never need to use this directly
@@ -167,6 +167,39 @@ app.exportContextFields('user');
167
167
 
168
168
  ```
169
169
 
170
+ ### Session
171
+ Session allows you to store temporary data for the users of your web application. You don't need to create an instance of Session, you will always use the instace `Application.session`.
172
+
173
+ Session data is tied to a visitor via sessionId, which is always available on `RequestContext`, which means you can interact with session data from routes and server side code of your components.
174
+
175
+ **Configuration**\
176
+ `StructuredConfig`.`session`:
177
+ ```
178
+ {
179
+ // cookie name for the session cookie
180
+ readonly cookieName: string,
181
+
182
+ // cookie stores the session key (a random string), keyLength determines it's length (longer key = more secure)
183
+ readonly keyLength: number,
184
+
185
+ // sessions expire after durationSeconds of no activity
186
+ readonly durationSeconds: number,
187
+
188
+ // session garbage collector runs every garbageCollectIntervalSeconds
189
+ // removing expired sessions from the memory
190
+ readonly garbageCollectIntervalSeconds: number
191
+ }
192
+ ```
193
+
194
+ **Methods**
195
+ - `setValue(sessionId: string, key: string, value: any): void` - set a session value for given sessionId
196
+ - `getValue<T>(sessionId: string, key: string): T | null` - return a value for given `key` from session, if `key` is not set, returns `null`. It is a generic method so you can specify the expected return type
197
+ - `removeValue(sessionId: string, key: string): void` - remove value for given `key`
198
+ - `getClear<T>(sessionId: string, key: string): T | null` - return and clear value for given `key`
199
+ - `clear(sessionId: string): void` - clear all data for given `sessionId`
200
+ - `extract(sessionId: string, keys: Array<string|{ [keyInSession: string] : string }>): LooseObject` - extract given keys from session and return them as an object. Key in `keys` can be a string in which case the key will remain the same in returned object or it can be an object { keyInSession : keyInReturnedData } in which case key in returned data will be keyInReturnedData
201
+
202
+
170
203
  ## Route
171
204
  Routes are the first thing that gets executed when your application receives a request. They are a mean for the developer to dictate what code gets executed depending on the URL. In addition to that, they allow capturing parts of the URL for use within the route.
172
205
 
@@ -199,7 +232,7 @@ Route file name has no effect on how the route (request handler) behaves, the on
199
232
  ### RequestContext
200
233
  All request handlers receive a `RequestContext` as the first argument.
201
234
  ```
202
- type RequestContext = {
235
+ type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
203
236
  request: IncomingMessage,
204
237
  response: ServerResponse,
205
238
  args: URIArguments,
@@ -208,7 +241,7 @@ type RequestContext = {
208
241
  cookies: Record<string, string>,
209
242
 
210
243
  // POSTed data, parsed to object
211
- body?: PostedDataDecoded,
244
+ body?: LooseObject,
212
245
 
213
246
  bodyRaw?: Buffer,
214
247
 
@@ -276,6 +309,21 @@ POST '/hello/(name)'
276
309
  **RegExp as URLPatter**\
277
310
  In some edge cases you may need more control of when a route is executed, in which case you can use a regular expression as URLPattern. If you use a RegExp, ctx.args will be `RegExpExecArray` so you can still capture data from the URL. This is very rarely needed because Structured router is versatile and covers almost all use cases.
278
311
 
312
+ > [!TIP]
313
+ > Since version 0.8.1 `Request`.`on` is a generic, accepting 0-2 generic arguments. First argument defines the request handler return type (response type) and defaults to any, second argument allows you to specify the expected (parsed) request body type, defaults to LooseObject.
314
+ > ```
315
+ > app.request.on<Document, {
316
+ > email: string,
317
+ > password: string,
318
+ > age: number
319
+ >}>('POST', '/users/create', asyc (ctx) => {
320
+ > ctx.body.email // string
321
+ > ctx.body.age // number
322
+ > const doc = new Document(ctx, 'User', app);
323
+ > return doc; // error if we return anything but Document
324
+ > });
325
+ > ```
326
+
279
327
  ## Document
280
328
  Document does not differ much from a component, in fact, it extends Component. It has a more user-firendly API than Component. Each Document represents a web page. It has a head and body. Structured intentionally does not differentiate between a page and a Component - page is just a component that loads many other components in a desired layout. DocumentHead (each document has one at Document.head) allows adding content to `<head>` section of the output HTML page.
281
329
 
@@ -337,7 +385,7 @@ You can now run the app and if you open /hello/world in the browser you will see
337
385
  `Hello, World!` - which came from your HelloWorld component.
338
386
 
339
387
  That was the simplest possible example, let's make it more interesting.
340
- Create a new file `/app/views/HelloWorld/HelloWorld.ts`:
388
+ Create a new file `/app/views/HelloWorld/HelloWorld.ts` (server side component code):
341
389
  ```
342
390
  import { ComponentScaffold } from 'system/Types.js';
343
391
  export default class HelloWorld implements ComponentScaffold {
@@ -450,6 +498,15 @@ which is now avaialble in `AnotherComponent` HTML, we assigned the received numb
450
498
 
451
499
  What about client side? **By default, data returned by server side code is not available in client side code** for obvious reasons, let's assume your server side code returns sensitive data such as user's password, you would not like that exposed on the client side, hence exporting data needs to be explicitly requested in the server side code. There are two ways to achieve this, setting `exportData = true` (exports all data), or `exportFields: Array<string> = [...keysToExport]` (export only given fields).
452
500
 
501
+ > [!NOTE]
502
+ > Whenever a component with server-side code is rendered, `getData` is automatically called and anything it returns is available in HTML. You can export all returned data to client-side code by setting `exportData = true` or you can export some of the fields by setting `exportFields = ["field1", "field2", ...]` as a direct property of the class. To access the exported data from client-side use `ClientComponent`.`getData(key: string)` which will be `this.getData(key:string)` within client side code.
503
+
504
+ > [!IMPORTANT]
505
+ > Server side `getData` will receive the following arguments:
506
+ > - `data: LooseObject` any data passed in (either by attributes, ClientComponent.add or ClientComponent.redraw)
507
+ > - `ctx: RequestContext` - current `RequestContext`, you will often use this to access for example ctx.data (`RequestContextData`) or ctx.sessionId to interact with session
508
+ > - `app: Application` - your Application instance. You can use it to, for example, access the session in combination with ctx.sessionId
509
+
453
510
  Let's create a client side code for `AnotherComponent` and export the `betterNumber` to it, create `/app/views/AnotherComponent/AnotherComponent.client.ts`:
454
511
  ```
455
512
  import { InitializerFunction } from 'system/Types.js';
package/build/Config.js CHANGED
@@ -19,8 +19,7 @@ export const config = {
19
19
  cookieName: 'session',
20
20
  keyLength: 24,
21
21
  durationSeconds: 60 * 60,
22
- garbageCollectIntervalSeconds: 60,
23
- garbageCollectAfterSeconds: 500
22
+ garbageCollectIntervalSeconds: 60
24
23
  },
25
24
  http: {
26
25
  port: 9191,
@@ -4,7 +4,7 @@ export default function (app) {
4
4
  app.request.on('GET', '/test/form', async (ctx) => {
5
5
  const doc = new Document(app, 'Form test', ctx);
6
6
  await doc.loadComponent('FormTestNested', { test: 3 });
7
- ctx.respondWith(doc);
7
+ return doc;
8
8
  });
9
9
  app.request.on('POST', '/test/form', async (ctx) => {
10
10
  console.log(JSON.stringify(ctx.body, undefined, 4));
@@ -24,7 +24,6 @@ export type StructuredConfig = {
24
24
  readonly keyLength: number;
25
25
  readonly durationSeconds: number;
26
26
  readonly garbageCollectIntervalSeconds: number;
27
- readonly garbageCollectAfterSeconds: number;
28
27
  };
29
28
  http: {
30
29
  host?: string;
@@ -38,21 +37,21 @@ export type StructuredClientConfig = {
38
37
  componentNameAttribute: string;
39
38
  };
40
39
  export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
41
- export type RequestCallback = (ctx: RequestContext) => Promise<any>;
40
+ export type RequestCallback<R extends any, Body extends LooseObject | undefined> = (ctx: RequestContext<Body>) => Promise<R>;
42
41
  export type RequestHandler = {
43
42
  match: Array<URISegmentPattern> | RegExp;
44
43
  methods: Array<RequestMethod>;
45
- callback: RequestCallback;
44
+ callback: RequestCallback<any, LooseObject | undefined>;
46
45
  scope: any;
47
46
  staticAsset: boolean;
48
47
  };
49
- export type RequestContext = {
48
+ export type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
50
49
  request: IncomingMessage;
51
50
  response: ServerResponse;
52
51
  args: URIArguments;
53
52
  handler: null | RequestHandler;
54
53
  cookies: Record<string, string>;
55
- body?: PostedDataDecoded;
54
+ body: Body;
56
55
  bodyRaw?: Buffer;
57
56
  files?: Record<string, RequestBodyRecordValue>;
58
57
  data: RequestContextData;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from 'path';
3
3
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
- const cwd = resolve(import.meta.dirname, '..');
4
+ const cwd = resolve(import.meta.dirname, '../../../');
5
5
  const projectRoot = resolve('.');
6
6
  if (process.argv.length < 3) {
7
7
  console.log('Thanks for using the Structured framework.');
@@ -1,12 +1,12 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
- import { PostedDataDecoded, RequestBodyFile, RequestCallback, RequestMethod } from "../Types.js";
2
+ import { LooseObject, PostedDataDecoded, RequestBodyFile, RequestCallback, RequestMethod } from "../Types.js";
3
3
  import { Application } from "./Application.js";
4
4
  export declare class Request {
5
5
  private app;
6
6
  constructor(app: Application);
7
- pageNotFoundCallback: RequestCallback;
7
+ pageNotFoundCallback: RequestCallback<void, PostedDataDecoded | undefined>;
8
8
  private readonly handlers;
9
- on(methods: RequestMethod | Array<RequestMethod>, pattern: string | RegExp | Array<string | RegExp>, callback: RequestCallback, scope?: any, isStaticAsset?: boolean): void;
9
+ on<R extends any, Body extends LooseObject | undefined = LooseObject>(methods: RequestMethod | Array<RequestMethod>, pattern: string | RegExp | Array<string | RegExp>, callback: RequestCallback<R, Body>, scope?: any, isStaticAsset?: boolean): void;
10
10
  private getHandler;
11
11
  handle(request: IncomingMessage, response: ServerResponse): Promise<void>;
12
12
  private extractURIArguments;
@@ -102,6 +102,7 @@ export class Request {
102
102
  handler,
103
103
  args: {},
104
104
  data: {},
105
+ body: undefined,
105
106
  getArgs,
106
107
  cookies: this.app.cookies.parse(request),
107
108
  isAjax: request.headers['x-requested-with'] == 'xmlhttprequest',
@@ -43,7 +43,7 @@ export class Session {
43
43
  }
44
44
  garbageCollect() {
45
45
  const time = new Date().getTime();
46
- const sessDurationMilliseconds = this.application.config.session.garbageCollectAfterSeconds * 1000;
46
+ const sessDurationMilliseconds = this.application.config.session.durationSeconds * 1000;
47
47
  for (const sessionId in this.sessions) {
48
48
  const sess = this.sessions[sessionId];
49
49
  if (time - sess.lastRequest > sessDurationMilliseconds) {
package/package.json CHANGED
@@ -14,13 +14,12 @@
14
14
  "license": "MIT",
15
15
  "type": "module",
16
16
  "main": "build/index",
17
- "version": "0.7.91",
17
+ "version": "0.8.1",
18
18
  "scripts": {
19
19
  "develop": "tsc --watch",
20
20
  "startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,css index.js",
21
21
  "start": "cd build && node index.js",
22
- "prepublish": "tsc",
23
- "postinstall": "echo \"Run npx structured init\" to create application boilerplate"
22
+ "prepublish": "tsc"
24
23
  },
25
24
  "bin": {
26
25
  "structured": "./build/system/bin/structured.js"
package/system/Types.ts CHANGED
@@ -24,8 +24,7 @@ export type StructuredConfig = {
24
24
  readonly cookieName: string,
25
25
  readonly keyLength: number,
26
26
  readonly durationSeconds: number,
27
- readonly garbageCollectIntervalSeconds: number,
28
- readonly garbageCollectAfterSeconds: number
27
+ readonly garbageCollectIntervalSeconds: number
29
28
  },
30
29
  http: {
31
30
  host?: string,
@@ -42,19 +41,19 @@ export type StructuredClientConfig = {
42
41
 
43
42
  export type RequestMethod = 'GET'|'POST'|'PUT'|'PATCH'|'DELETE';
44
43
 
45
- export type RequestCallback = (ctx: RequestContext) => Promise<any>
44
+ export type RequestCallback<R extends any, Body extends LooseObject | undefined> = (ctx: RequestContext<Body>) => Promise<R>
46
45
 
47
46
  export type RequestHandler = {
48
47
  match: Array<URISegmentPattern>|RegExp,
49
48
  methods: Array<RequestMethod>,
50
- callback: RequestCallback,
49
+ callback: RequestCallback<any, LooseObject | undefined>,
51
50
  scope: any,
52
51
 
53
52
  // if true, no (before/after)RequestHandler event is emitted, body and GET args not parsed
54
53
  staticAsset: boolean
55
54
  }
56
55
 
57
- export type RequestContext = {
56
+ export type RequestContext<Body extends LooseObject | undefined = LooseObject> = {
58
57
  request: IncomingMessage,
59
58
  response: ServerResponse,
60
59
  args: URIArguments,
@@ -63,7 +62,7 @@ export type RequestContext = {
63
62
  cookies: Record<string, string>,
64
63
 
65
64
  // POSTed data, parsed to object
66
- body?: PostedDataDecoded,
65
+ body: Body,
67
66
 
68
67
  bodyRaw?: Buffer,
69
68
 
@@ -5,7 +5,8 @@
5
5
  import { resolve } from 'path';
6
6
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
7
 
8
- const cwd = resolve(import.meta.dirname, '..');
8
+ // we are in /build/system/bin when this runs, go up 3 levels so cwd points to project root
9
+ const cwd = resolve(import.meta.dirname, '../../../');
9
10
  const projectRoot = resolve('.');
10
11
 
11
12
  if (process.argv.length < 3) {
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
- import { PostedDataDecoded, RequestBodyFile, RequestCallback, RequestContext, RequestHandler, RequestMethod, URIArguments, URISegmentPattern } from "../Types.js";
2
+ import { LooseObject, PostedDataDecoded, RequestBodyFile, RequestCallback, RequestContext, RequestHandler, RequestMethod, URIArguments, URISegmentPattern } from "../Types.js";
3
3
  import { mergeDeep, queryStringDecode, queryStringDecodedSetValue } from "../Util.js";
4
4
  import { Application } from "./Application.js";
5
5
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
@@ -15,7 +15,7 @@ export class Request {
15
15
  this.app = app;
16
16
  }
17
17
 
18
- pageNotFoundCallback: RequestCallback = async ({ response }) => {
18
+ pageNotFoundCallback: RequestCallback<void, PostedDataDecoded | undefined> = async ({ response }) => {
19
19
  response.statusCode = 404;
20
20
  response.write('Page not found');
21
21
  response.end();
@@ -30,10 +30,10 @@ export class Request {
30
30
  // callback.this will be the scope if scope is provided, otherwise scope is the Application instance
31
31
  // if pattern is given as array, one request handler will be created for each element of the array
32
32
  // if isStaticAsset = true, (before/after)RequestHandler event not emitted, body and GET args not parsed
33
- public on(
33
+ public on<R extends any, Body extends LooseObject | undefined = LooseObject>(
34
34
  methods: RequestMethod|Array<RequestMethod>,
35
35
  pattern: string|RegExp|Array<string|RegExp>,
36
- callback: RequestCallback,
36
+ callback: RequestCallback<R, Body>,
37
37
  scope?: any,
38
38
  isStaticAsset: boolean = false
39
39
  ): void {
@@ -163,7 +163,7 @@ export class Request {
163
163
  // get the best matching request handler
164
164
  const handler = this.getHandler(uri, requestMethod);
165
165
 
166
- const context: RequestContext = {
166
+ const context: RequestContext<LooseObject | undefined> = {
167
167
  request,
168
168
  response,
169
169
  handler,
@@ -173,6 +173,7 @@ export class Request {
173
173
  // potentially falsely declared as RequestContextData
174
174
  // user will fill this out, usually on beforeRequestHandler
175
175
  data: {} as RequestContextData,
176
+ body: undefined,
176
177
  getArgs,
177
178
  cookies: this.app.cookies.parse(request),
178
179
  isAjax : request.headers['x-requested-with'] == 'xmlhttprequest',
@@ -347,7 +348,7 @@ export class Request {
347
348
  // parse raw request body
348
349
  // if there is a parser for received Content-Type
349
350
  // then ctx.body is populated with data: URIArgs
350
- private async parseBody(ctx: Omit<RequestContext, 'data'>): Promise<void> {
351
+ private async parseBody(ctx: RequestContext<LooseObject | undefined>): Promise<void> {
351
352
  if (ctx.request.headers['content-type']) {
352
353
 
353
354
  ctx.bodyRaw = await this.dataRaw(ctx.request);
@@ -73,7 +73,7 @@ export class Session {
73
73
  // remove expired session entries
74
74
  private garbageCollect(): void {
75
75
  const time = new Date().getTime();
76
- const sessDurationMilliseconds = this.application.config.session.garbageCollectAfterSeconds * 1000;
76
+ const sessDurationMilliseconds = this.application.config.session.durationSeconds * 1000;
77
77
 
78
78
  for (const sessionId in this.sessions) {
79
79
  const sess = this.sessions[sessionId];