structured-fw 0.7.5 → 0.7.7
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 +1 -1
- package/README.md +247 -5
- package/bin/structured +1 -1
- package/build/Config.d.ts +1 -1
- package/build/app/routess/Auth.d.ts +0 -0
- package/build/app/routess/Auth.js +1 -0
- package/build/app/routess/Test.d.ts +2 -0
- package/build/app/routess/Test.js +101 -0
- package/build/app/routess/Todo.d.ts +0 -0
- package/build/app/routess/Todo.js +1 -0
- package/build/app/routess/Upload.d.ts +0 -0
- package/build/app/routess/Upload.js +1 -0
- package/build/app/routess/Validation.d.ts +2 -0
- package/build/app/routess/Validation.js +34 -0
- package/build/index.js +1 -1
- package/build/system/server/Application.d.ts +1 -1
- package/build/system/server/Components.js +3 -0
- package/build/system/server/Request.js +3 -0
- package/index.ts +1 -1
- package/package.json +8 -2
- package/system/server/Application.ts +1 -1
- package/system/server/Components.ts +3 -0
- package/system/server/Request.ts +5 -0
- package/tsconfig.json +1 -1
package/Config.ts
CHANGED
package/README.md
CHANGED
|
@@ -7,32 +7,274 @@ It works with Node.js and Deno runtimes. Other runtimes are not tested.
|
|
|
7
7
|
|
|
8
8
|
- [Why Structured](#why-structured)
|
|
9
9
|
- [Audience](#audience)
|
|
10
|
-
|
|
10
|
+
- [Getting started](#getting-started)
|
|
11
11
|
|
|
12
12
|
### Key concepts:
|
|
13
|
+
* [Application](#application)
|
|
13
14
|
* [Route](#route)
|
|
14
15
|
* [Document](#document)
|
|
15
16
|
* [ClientComponent](#component) (component)
|
|
16
17
|
|
|
18
|
+
## Getting started
|
|
19
|
+
|
|
20
|
+
### Initialize a Node.js project
|
|
21
|
+
```
|
|
22
|
+
cd /path/to/project
|
|
23
|
+
npm init -y
|
|
24
|
+
npm install @types/node
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
*If you have TypeScript installed globally then you can skip the following*\
|
|
28
|
+
`npm install --save-dev typescript`
|
|
29
|
+
|
|
30
|
+
### Install Structured
|
|
31
|
+
`npm install structured-fw`
|
|
32
|
+
|
|
33
|
+
### Create boilerplate
|
|
34
|
+
`npx structured init`
|
|
35
|
+
|
|
36
|
+
### Compile
|
|
37
|
+
`tsc`\
|
|
38
|
+
This will create a directory `build` (or whatever you have in tsconfig.json as compilerOptions.outputDir)
|
|
39
|
+
|
|
40
|
+
### Run
|
|
41
|
+
```
|
|
42
|
+
cd build
|
|
43
|
+
node index.js
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Of course, you can use pm2 or other process managers to run it, with pm2:
|
|
47
|
+
```
|
|
48
|
+
cd build
|
|
49
|
+
pm2 start index.js --name="[appName]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
# Key concepts
|
|
53
|
+
|
|
54
|
+
## Application
|
|
55
|
+
Application instance is the base of any Structured application. You will usually create an instance of Application in index.ts (or whatever you decide to be the entry point file name). Application starts a http server, creates instances of all classes that are required for the functioning of your application and allows handling of various events that will occur when your app is running.\
|
|
56
|
+
Application constructor requires one argument of type `StructuredConfig`:
|
|
57
|
+
```
|
|
58
|
+
type StructuredConfig = {
|
|
59
|
+
readonly envPrefix?: string,
|
|
60
|
+
readonly autoInit: boolean,
|
|
61
|
+
url: {
|
|
62
|
+
removeTrailingSlash: boolean,
|
|
63
|
+
componentRender: false | string,
|
|
64
|
+
isAsset: (url: string) => boolean
|
|
65
|
+
},
|
|
66
|
+
routes: {
|
|
67
|
+
readonly path: string
|
|
68
|
+
},
|
|
69
|
+
components: {
|
|
70
|
+
readonly path: string,
|
|
71
|
+
readonly componentNameAttribute: string
|
|
72
|
+
},
|
|
73
|
+
session: {
|
|
74
|
+
readonly cookieName: string,
|
|
75
|
+
readonly keyLength: number,
|
|
76
|
+
readonly durationSeconds: number,
|
|
77
|
+
readonly garbageCollectIntervalSeconds: number,
|
|
78
|
+
readonly garbageCollectAfterSeconds: number
|
|
79
|
+
},
|
|
80
|
+
http: {
|
|
81
|
+
host?: string,
|
|
82
|
+
port: number,
|
|
83
|
+
linkHeaderRel: 'preload' | 'preconnect'
|
|
84
|
+
},
|
|
85
|
+
readonly runtime: 'Node.js' | 'Deno'
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If you created the boilerplate using `npx structured init` then a sample `Config.ts` has been created in the project root. You can read the comments there if you need clarification on what each config option affects.
|
|
90
|
+
|
|
91
|
+
The most basic entry point may look something like this:
|
|
92
|
+
```
|
|
93
|
+
import { Application } from "structured-fw/Application";
|
|
94
|
+
import { config } from "./Config.js";
|
|
95
|
+
|
|
96
|
+
new Application(config);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Properties
|
|
100
|
+
- `cookies` - Instance of Cookies, allows you to set a cookie
|
|
101
|
+
- `session` - Instance of Session, utilities to manage sessions and data
|
|
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
|
+
- `handlebars` - Instance of Handlebars (wrapper around Handlebars templating engine)
|
|
104
|
+
- `components` - Instance of Components, this is the components registry, you should never need to use this directly
|
|
105
|
+
|
|
106
|
+
### Methods
|
|
107
|
+
- `init(): Promise<void>` - initializes application, you only need to run this if you set `autoInit = false` in config, otherwise this will be ran when you create the Application instance
|
|
108
|
+
- `on(evt: ApplicationEvents, callback: RequestCallback|((payload?: any) => void))` - allows you to add event listeners for specific `ApplicationEvenets`:
|
|
109
|
+
- `serverStarted` - executed once the built-in http server is started and running. Callback receives no arguments
|
|
110
|
+
- `beforeRequestHandler` - runs before any request handler (route) is executed. Callback receives `RequestContext` as the first argument. Useful for example to set `RequestContext.data: RequestContextData` (user defined data, to make it available to routes and components)
|
|
111
|
+
- `afterRequestHandler` - runs after any request handler (route) is executed. Callback receives `RequestContext` as the first argument
|
|
112
|
+
- `afterRoutes` - runs after all routes are loaded from `StructuredConfig.routes.path`. Callback receives no arguments
|
|
113
|
+
- `beforeComponentLoad` - runs before components are loaded from `StructuredConfig.components.path`. Callback receives no arguments
|
|
114
|
+
- `afterComponentLoad` - runs after all components are loaded from `StructuredConfig.components.path`. Callback receives no arguments
|
|
115
|
+
- `documentCreated` - runs whenever an instance of a [Document](#document) is created. Callback receives the Document instance as the first argument. You will often use this, for example if you want to include a CSS file to all pages `Document.head.addCSS(...)`
|
|
116
|
+
- `beforeAssetAccess` - runs when assets are being accessed, before response is sent. Callback receives `RequestContext` as the first argument
|
|
117
|
+
- `afterAssetAccess` - runs when assets are being accessed, after response is sent. Callback receives `RequestContext` as the first argument
|
|
118
|
+
- `pageNotFound` - runs when a request is received for which there is no registered request handler (route), and the requested URL is not an asset. Callback receives `RequestContext` as the first argument
|
|
119
|
+
- **Callback to any of the `ApplicationEvents` is expected to be an async function**
|
|
120
|
+
- `importEnv<T extends LooseObject>(smartPrimitives: boolean = true): T` - import ENV variables that start with `StructuredConfig.envPrefix`_ (if envPrefix is omitted from config, all ENV variables are returned). It is a generic method so that you can specify the expected return type. If `smartPrimitives = true` importEnv will convert the ENV values to type it feels is appropriate:
|
|
121
|
+
- numeric values -> `number`
|
|
122
|
+
- "true"|"false" -> `boolean`
|
|
123
|
+
- "null" -> `null`
|
|
124
|
+
- "undefined" -> `undefined`
|
|
125
|
+
- `exportContextFields(...fields: Array<keyof RequestContextData>): void` - allows you to export any fields from `RequestContextData` to all components (even if they don't have server side code)
|
|
126
|
+
|
|
127
|
+
What your entry point may look like in a real-world application:
|
|
128
|
+
```
|
|
129
|
+
import { Application } from "structured-fw/Application";
|
|
130
|
+
import { config } from './Config.js';
|
|
131
|
+
import { userModel } from './app/models/User.js';
|
|
132
|
+
|
|
133
|
+
const app = new Application(config);
|
|
134
|
+
|
|
135
|
+
const env = app.importEnv<{ COOKIE_AUTOLOGIN: string }>();
|
|
136
|
+
|
|
137
|
+
app.on('documentCreated', (doc: Document) => {
|
|
138
|
+
doc.head.setFavicon({
|
|
139
|
+
image: '/assets/img/favicon.png',
|
|
140
|
+
type: 'image/png'
|
|
141
|
+
});
|
|
142
|
+
doc.head.addCSS('/assets/css/dist.css', 0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
app.on('beforeRequestHandler', async (ctx: RequestContext) => {
|
|
146
|
+
|
|
147
|
+
// set ctx.data.user from session
|
|
148
|
+
ctx.data.user = app.session.getValue<User>(ctx.sessionId, 'user');
|
|
149
|
+
|
|
150
|
+
if (! ctx.data.user) {
|
|
151
|
+
// check if user has an autologinKey cookie set
|
|
152
|
+
const autologinCookie = ctx.cookies[env.COOKIE_AUTOLOGIN];
|
|
153
|
+
if (autologinCookie) {
|
|
154
|
+
const user = await userModel.getByAutologinKey(autologinCookie);
|
|
155
|
+
if (user) {
|
|
156
|
+
ctx.data.user = user;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// load handlebars helpers (which will become available in all components)
|
|
163
|
+
app.handlebars.loadHelpers(path.resolve('./app/Helpers.js'));
|
|
164
|
+
|
|
165
|
+
// make user available to all components
|
|
166
|
+
app.exportContextFields('user');
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
|
|
17
170
|
## Route
|
|
18
171
|
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.
|
|
19
|
-
|
|
20
|
-
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
You can add routes from your entry point using `app.request.on(RequestMethod, URLPattern, requestHandler)`, but you will never want to do that unless your entire application has a very few routes, in which case it would be acceptable.
|
|
175
|
+
|
|
21
176
|
**Simple route:**
|
|
22
177
|
```
|
|
23
178
|
app.request.on('GET', '/hello/world', async () => {
|
|
24
179
|
return 'Hello, world!';
|
|
25
180
|
});
|
|
26
181
|
```
|
|
27
|
-
|
|
28
|
-
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
In a real life situation, you will likely have quite a few routes that you want to handle, and it usually makes sense to group them in multiple files, for example Auth.ts, Users.ts, Products.ts, etc...
|
|
185
|
+
When Application instance is created and initialized, it will load all routes from `conf.routes.path`.
|
|
186
|
+
|
|
187
|
+
All route files need to export a function that will receive the Application instance as the first argument:
|
|
188
|
+
```
|
|
189
|
+
import { Application } from "structured-fw/Application";
|
|
190
|
+
|
|
191
|
+
export default function(app: Application) {
|
|
192
|
+
// all routes that belong to this file come here
|
|
193
|
+
app.request.on(...)
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Route file name has no effect on how the route (request handler) behaves, the only purpose of splitting your routes in separate files is making your code more maintainable.
|
|
198
|
+
|
|
199
|
+
### RequestContext
|
|
200
|
+
All request handlers receive a `RequestContext` as the first argument.
|
|
201
|
+
```
|
|
202
|
+
type RequestContext = {
|
|
203
|
+
request: IncomingMessage,
|
|
204
|
+
response: ServerResponse,
|
|
205
|
+
args: URIArguments,
|
|
206
|
+
handler: null|RequestHandler,
|
|
207
|
+
|
|
208
|
+
cookies: Record<string, string>,
|
|
209
|
+
|
|
210
|
+
// POSTed data, parsed to object
|
|
211
|
+
body?: PostedDataDecoded,
|
|
212
|
+
|
|
213
|
+
bodyRaw?: Buffer,
|
|
214
|
+
|
|
215
|
+
// files extracted from request body
|
|
216
|
+
files?: Record<string, RequestBodyRecordValue>,
|
|
217
|
+
|
|
218
|
+
// user defined data
|
|
219
|
+
data: RequestContextData,
|
|
220
|
+
|
|
221
|
+
// if session is started and user has visited any page
|
|
222
|
+
sessionId?: string,
|
|
223
|
+
|
|
224
|
+
// true if x-requested-with header is received and it equals 'xmlhttprequest'
|
|
225
|
+
isAjax: boolean,
|
|
226
|
+
|
|
227
|
+
// URL GET arguments
|
|
228
|
+
getArgs: PostedDataDecoded,
|
|
229
|
+
|
|
230
|
+
// send given data as a response
|
|
231
|
+
respondWith: (data: any) => void,
|
|
232
|
+
|
|
233
|
+
// redirect to given url, with given statusCode
|
|
234
|
+
redirect: (to: string, statusCode?: number) => void,
|
|
235
|
+
|
|
236
|
+
// show a 404 page
|
|
237
|
+
show404: () => Promise<void>
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Capture URL segment:**\
|
|
242
|
+
Any URL segments in parenthesis will become available in ctx.args. For example:
|
|
29
243
|
```
|
|
30
244
|
app.request.on('GET', '/greet/(name)', async (ctx) => {
|
|
31
245
|
return `Hello, ${ctx.args.name}!`;
|
|
32
246
|
});
|
|
33
247
|
```
|
|
248
|
+
You can capture any number of URL segments in this way.
|
|
249
|
+
|
|
250
|
+
**Capture group modifiers:**\
|
|
251
|
+
Capture group in URL pattern is (name) in above example. It makes data available within your route. Name will capture any string. Sometimes we know we expect a number in our URLs, in which case it is useful to use the modifier :num (which is the only modifier available), for example:
|
|
252
|
+
```
|
|
253
|
+
app.request.on('GET', '/greet/(userId:num)', async (ctx) => {
|
|
254
|
+
const userId = ctx.args.userId as number;
|
|
255
|
+
// fetch user from DB
|
|
256
|
+
const user = await userModel.get(userId);
|
|
257
|
+
return `Hello, ${user.name}!`;
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
It is safe to cast `ctx.args.userId` as `number` in above example because the route would not get executed if the second segment of the URL is not a numeric value, and in case :num modifier is used, URL-provided value is parsed to a number and you don't need to parseInt manually.
|
|
34
261
|
|
|
35
262
|
|
|
263
|
+
**Doing more with less code**\
|
|
264
|
+
You can have the same route be executed for multiple different request methods or URLs. Both request method (first argument) and URL pattern (second argument) can be an array.
|
|
265
|
+
```
|
|
266
|
+
app.request.on(['GET', 'POST'], ['/greet/(name)', '/hello/(name)'], async (ctx) => {
|
|
267
|
+
return `Hello, ${ctx.args.name}!`;
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
Above is equivalent of registering 4 request handlers one-by-one:\
|
|
271
|
+
GET '/greet/(name)'\
|
|
272
|
+
POST '/greet/(name)'\
|
|
273
|
+
GET '/hello/(name)'\
|
|
274
|
+
POST '/hello/(name)'
|
|
275
|
+
|
|
276
|
+
**RegExp as URLPatter**\
|
|
277
|
+
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.
|
|
36
278
|
|
|
37
279
|
## Document
|
|
38
280
|
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.
|
package/bin/structured
CHANGED
|
@@ -78,7 +78,7 @@ function createTsconfig() {
|
|
|
78
78
|
"strictNullChecks": true,
|
|
79
79
|
"strictPropertyInitialization": true,
|
|
80
80
|
"strictBindCallApply": true,
|
|
81
|
-
"moduleResolution" : "
|
|
81
|
+
"moduleResolution" : "bundler",
|
|
82
82
|
"outDir": "./build",
|
|
83
83
|
"module": "ES2020",
|
|
84
84
|
"target": "ES2021",
|
package/build/Config.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { StructuredConfig } from 'structured-fw/
|
|
1
|
+
import { StructuredConfig } from 'structured-fw/Types';
|
|
2
2
|
export declare const config: StructuredConfig;
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Document } from '../../system/server/Document.js';
|
|
2
|
+
import { Request } from '../../system/server/Request.js';
|
|
3
|
+
export default function (app) {
|
|
4
|
+
app.request.on('GET', '/test/form', async (ctx) => {
|
|
5
|
+
const doc = new Document(app, 'Form test', ctx);
|
|
6
|
+
await doc.loadComponent('FormTestNested', { test: 3 });
|
|
7
|
+
ctx.respondWith(doc);
|
|
8
|
+
});
|
|
9
|
+
app.request.on('POST', '/test/form', async (ctx) => {
|
|
10
|
+
console.log(JSON.stringify(ctx.body, undefined, 4));
|
|
11
|
+
const userImage = ctx.files.user.image[0];
|
|
12
|
+
ctx.response.setHeader('Content-Type', userImage.type);
|
|
13
|
+
ctx.respondWith(userImage.data);
|
|
14
|
+
});
|
|
15
|
+
app.request.on('GET', '/test/client_import', async (ctx) => {
|
|
16
|
+
const doc = new Document(app, 'Test', ctx);
|
|
17
|
+
await doc.loadComponent('ClientImport', { xyz: 10, asd: 12 });
|
|
18
|
+
ctx.respondWith(doc);
|
|
19
|
+
});
|
|
20
|
+
app.request.on('GET', '/test/redraw', async (ctx) => {
|
|
21
|
+
const doc = new Document(app, 'Test', ctx);
|
|
22
|
+
await doc.loadComponent('RedrawAbort');
|
|
23
|
+
ctx.respondWith(doc);
|
|
24
|
+
});
|
|
25
|
+
app.request.on('GET', '/test/models', async (ctx) => {
|
|
26
|
+
const doc = new Document(app, 'Test', ctx);
|
|
27
|
+
await doc.loadComponent('ModelsTest');
|
|
28
|
+
ctx.respondWith(doc);
|
|
29
|
+
});
|
|
30
|
+
app.request.on('GET', '/getargs', async (ctx) => {
|
|
31
|
+
ctx.respondWith(ctx.getArgs);
|
|
32
|
+
});
|
|
33
|
+
app.request.on('GET', '/form/multipart', async (ctx) => {
|
|
34
|
+
const doc = new Document(app, 'Test multipart form', ctx);
|
|
35
|
+
await doc.loadComponent('MultipartForm');
|
|
36
|
+
ctx.respondWith(doc);
|
|
37
|
+
});
|
|
38
|
+
app.request.on('POST', '/form/multipart', async (ctx) => {
|
|
39
|
+
console.log(ctx.files);
|
|
40
|
+
ctx.respondWith(ctx.body);
|
|
41
|
+
});
|
|
42
|
+
app.request.on('GET', '/conditional', async (ctx) => {
|
|
43
|
+
const doc = new Document(app, 'Test multipart form', ctx);
|
|
44
|
+
await doc.loadComponent('Conditionals');
|
|
45
|
+
ctx.respondWith(doc);
|
|
46
|
+
});
|
|
47
|
+
app.request.on('GET', '/urldecode', async (ctx) => {
|
|
48
|
+
const noVal = 'noVal';
|
|
49
|
+
const noValNested = 'filters[beds][min]';
|
|
50
|
+
const simple = 'simple=2';
|
|
51
|
+
const simpleObj = 'person[name]=fname&person[last_name]=lname';
|
|
52
|
+
const simpleArray = 'colors[]=red&colors[]=blue';
|
|
53
|
+
const orderedArray = 'colorsOrdered[1]=red&colorsOrdered[0]=blue';
|
|
54
|
+
const indexedArray = 'months[0]=Jan&months[1]=Feb&months[2]=Mar';
|
|
55
|
+
const arrayOfObjects = 'users[0][name]=John&users[0][email]=johndoe@gmail.com&users[1][name]=Tim&users[1][email]=tim@gmail.com';
|
|
56
|
+
const objectWithArrayValues = 'user[name]=John&user[last_name]=Doe&user[sports][]=table tennis&user[sports][]=football';
|
|
57
|
+
const objectWithObjectValues = 'data[paper][props][size]=10x13&data[paper][props][type]=matte';
|
|
58
|
+
const nestedArraySimple = 'colorStack[0][]=red&colorStack[0][]=blue&colorStack[1][]=green';
|
|
59
|
+
const arrayOfObjectsWithArrayValues = 'usersArr[0][name]=John&usersArr[0][email]=johndoe@gmail.com&usersArr[0][sports][]=football&usersArr[0][sports][]=basketball&usersArr[1][name]=Too&usersArr[1][email]=tootoo@gmail.com&usersArr[1][sports][]=bocce&usersArr[1][sports][]=cricket&usersArr[1][sports][]=dancing';
|
|
60
|
+
const missingValue = 'missing=';
|
|
61
|
+
const missingArrayValue = 'missingArr[]=';
|
|
62
|
+
const objBlankValue = `objBlank[0][email]=`;
|
|
63
|
+
const spaces = `spaces=value%20with%20spaces`;
|
|
64
|
+
const special = `garbage=` + encodeURIComponent('value!@#&$%^*()');
|
|
65
|
+
const nonLatin = `key3=` + encodeURIComponent('привет');
|
|
66
|
+
const t = new Date().getTime();
|
|
67
|
+
const test = Request.queryStringDecode([noVal, noValNested, simple, simpleObj, simpleArray, orderedArray, indexedArray, arrayOfObjects, objectWithArrayValues, objectWithObjectValues, nestedArraySimple, arrayOfObjectsWithArrayValues, missingValue, nonLatin, spaces, special, objBlankValue, missingArrayValue, simple].join('&'));
|
|
68
|
+
const dur = new Date().getTime() - t;
|
|
69
|
+
console.log(dur);
|
|
70
|
+
ctx.respondWith(test);
|
|
71
|
+
});
|
|
72
|
+
app.request.on('GET', '/client_import', async (ctx) => {
|
|
73
|
+
const doc = new Document(app, 'Test multipart form', ctx);
|
|
74
|
+
await doc.loadComponent('ClientImport');
|
|
75
|
+
doc.head.add(`
|
|
76
|
+
<script type="importmap">
|
|
77
|
+
{
|
|
78
|
+
imports: {
|
|
79
|
+
'components/*' : '/build/app/views/components/*'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
`);
|
|
84
|
+
ctx.respondWith(doc);
|
|
85
|
+
});
|
|
86
|
+
app.request.on('GET', '/serverclass', async (ctx) => {
|
|
87
|
+
const doc = new Document(app, 'Test multipart form', ctx);
|
|
88
|
+
await doc.loadComponent('ServerSideContext');
|
|
89
|
+
ctx.respondWith(doc);
|
|
90
|
+
});
|
|
91
|
+
app.request.on('GET', '/routes/new', async (ctx) => {
|
|
92
|
+
const doc = new Document(app, 'Test multipart form', ctx);
|
|
93
|
+
await doc.loadComponent('Conditionals');
|
|
94
|
+
return doc;
|
|
95
|
+
});
|
|
96
|
+
app.request.on('GET', '/passObj', async (ctx) => {
|
|
97
|
+
const doc = new Document(app, 'Test pass obj', ctx);
|
|
98
|
+
await doc.loadComponent('PassObject');
|
|
99
|
+
ctx.respondWith(doc);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FormValidation } from '../../system/server/FormValidation.js';
|
|
2
|
+
import { Document } from "../../system/server/Document.js";
|
|
3
|
+
export default function (app) {
|
|
4
|
+
let validator = new FormValidation();
|
|
5
|
+
validator.singleError = true;
|
|
6
|
+
validator.addRule('name', 'Name', ['required', ['minLength', 3]]);
|
|
7
|
+
validator.addRule('email', 'Email', ['validEmail']);
|
|
8
|
+
validator.addRule('number', 'Number', ['number', 'required']);
|
|
9
|
+
validator.addRule('numeric', 'Numeric', ['numeric', 'required']);
|
|
10
|
+
validator.addRule('float', 'Float', ['float', 'required']);
|
|
11
|
+
app.request.on('POST', '/validation', async (ctx) => {
|
|
12
|
+
if (ctx.body) {
|
|
13
|
+
let validationResult = await validator.validate(ctx.body);
|
|
14
|
+
if (validationResult.valid) {
|
|
15
|
+
ctx.response.write('Valid');
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
app.session.setValue(ctx.sessionId, 'validationErrors', validationResult.errors);
|
|
19
|
+
app.session.setValue(ctx.sessionId, 'formValues', ctx.body);
|
|
20
|
+
app.request.redirect(ctx.response, '/validation');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
app.request.on('GET', '/validation', async (ctx) => {
|
|
25
|
+
let doc = new Document(app, 'Form validation');
|
|
26
|
+
await doc.loadView('pages/validation', app.session.extract(ctx.sessionId, [
|
|
27
|
+
{ validationErrors: 'errors' },
|
|
28
|
+
{ formValues: 'values' }
|
|
29
|
+
]));
|
|
30
|
+
ctx.response.write(doc.toString());
|
|
31
|
+
app.session.removeValue(ctx.sessionId, 'validationErrors');
|
|
32
|
+
app.session.removeValue(ctx.sessionId, 'formValues');
|
|
33
|
+
});
|
|
34
|
+
}
|
package/build/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export declare class Application {
|
|
|
19
19
|
readonly exportedRequestContextData: Array<keyof RequestContextData>;
|
|
20
20
|
constructor(config: StructuredConfig);
|
|
21
21
|
init(): Promise<void>;
|
|
22
|
-
start
|
|
22
|
+
private start;
|
|
23
23
|
on(evt: ApplicationEvents, callback: RequestCallback | ((payload?: any) => void)): void;
|
|
24
24
|
emit(eventName: ApplicationEvents, payload?: any): Promise<Array<any>>;
|
|
25
25
|
importEnv<T extends LooseObject>(smartPrimitives?: boolean): T;
|
|
@@ -9,6 +9,9 @@ export class Components {
|
|
|
9
9
|
loadComponents(relativeToPath) {
|
|
10
10
|
if (relativeToPath === undefined) {
|
|
11
11
|
relativeToPath = path.resolve((this.config.runtime === 'Node.js' ? '../' : './') + this.config.components.path);
|
|
12
|
+
if (!existsSync(relativeToPath)) {
|
|
13
|
+
throw new Error(`Components path not found, expected to find:\n${relativeToPath}`);
|
|
14
|
+
}
|
|
12
15
|
}
|
|
13
16
|
const components = readdirSync(relativeToPath);
|
|
14
17
|
components.forEach(async (component) => {
|
|
@@ -293,6 +293,9 @@ export class Request {
|
|
|
293
293
|
else {
|
|
294
294
|
routesPath = path.resolve((this.app.config.runtime === 'Node.js' ? '../build/' : './') + this.app.config.routes.path);
|
|
295
295
|
}
|
|
296
|
+
if (!existsSync(routesPath)) {
|
|
297
|
+
throw new Error(`Routes path not found, expected to find:\n${routesPath}`);
|
|
298
|
+
}
|
|
296
299
|
const files = readdirSync(routesPath);
|
|
297
300
|
for (let i = 0; i < files.length; i++) {
|
|
298
301
|
const file = files[i];
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "build/index",
|
|
11
|
-
"version": "0.7.
|
|
11
|
+
"version": "0.7.7",
|
|
12
12
|
"scripts": {
|
|
13
13
|
"develop": "tsc --watch",
|
|
14
14
|
"startDev": "cd build && nodemon --watch '../app/**/*' --watch '../build/**/*' -e js,html,css index.js",
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
"ts-md5": "^1.3.1"
|
|
31
31
|
},
|
|
32
32
|
"exports" : {
|
|
33
|
-
"./
|
|
33
|
+
"./Types": "./build/system/Types.js",
|
|
34
|
+
"./Symbols": "./build/system/Symbols.js",
|
|
35
|
+
"./Util": "./build/system/Util.js",
|
|
36
|
+
"./Application": "./build/system/server/Application.js",
|
|
37
|
+
"./Document": "./build/system/server/Document.js",
|
|
38
|
+
"./FormValidation": "./build/system/server/FormValidation.js",
|
|
39
|
+
"./ClientComponent": "./build/system/client/ClientComponent.js"
|
|
34
40
|
}
|
|
35
41
|
}
|
|
@@ -100,7 +100,7 @@ export class Application {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// start the http server
|
|
103
|
-
|
|
103
|
+
private start(): Promise<void> {
|
|
104
104
|
return new Promise((resolve, reject) => {
|
|
105
105
|
this.server = createServer((req, res) => {
|
|
106
106
|
this.request.handle(req, res);
|
|
@@ -18,6 +18,9 @@ export class Components {
|
|
|
18
18
|
public loadComponents(relativeToPath?: string): void {
|
|
19
19
|
if (relativeToPath === undefined) {
|
|
20
20
|
relativeToPath = path.resolve((this.config.runtime === 'Node.js' ? '../' : './') + this.config.components.path);
|
|
21
|
+
if (! existsSync(relativeToPath)) {
|
|
22
|
+
throw new Error(`Components path not found, expected to find:\n${relativeToPath}`);
|
|
23
|
+
}
|
|
21
24
|
}
|
|
22
25
|
const components = readdirSync(relativeToPath);
|
|
23
26
|
|
package/system/server/Request.ts
CHANGED
|
@@ -420,6 +420,11 @@ export class Request {
|
|
|
420
420
|
} else {
|
|
421
421
|
routesPath = path.resolve((this.app.config.runtime === 'Node.js' ? '../build/' : './') + this.app.config.routes.path);
|
|
422
422
|
}
|
|
423
|
+
|
|
424
|
+
if (! existsSync(routesPath)) {
|
|
425
|
+
throw new Error(`Routes path not found, expected to find:\n${routesPath}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
423
428
|
const files = readdirSync(routesPath);
|
|
424
429
|
|
|
425
430
|
for (let i = 0; i < files.length; i++) {
|