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,163 @@
1
+ import { ServerResponse } from 'node:http';
2
+ import { Md5 } from 'ts-md5';
3
+
4
+ import { Initializers, LooseObject, RequestContext, StructuredClientConfig } from '../../system/Types.js';
5
+ import { Application } from './Application.js';
6
+ import { DocumentHead } from './DocumentHead.js';
7
+ import { Component } from './Component.js';
8
+ import { attributeValueToString, randomString } from '../Util.js';
9
+ import path from 'node:path';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+
12
+ export class Document extends Component {
13
+
14
+ head: DocumentHead;
15
+ language = 'en';
16
+ application: Application;
17
+
18
+ initializers: Initializers = {};
19
+ initializersInitialized: boolean = false;
20
+
21
+ componentIds: Array<string> = [];
22
+
23
+ ctx: undefined|RequestContext;
24
+
25
+ appendHTML: string = '';
26
+
27
+ constructor(app: Application, title: string, ctx?: RequestContext) {
28
+ super('root');
29
+ this.application = app;
30
+ this.ctx = ctx;
31
+ this.document = this;
32
+ this.head = new DocumentHead(title);
33
+
34
+ // include client side JS, not an actual URL, Application.ts adds a request handler
35
+ // for routes starting with /assets/client-js/
36
+ this.head.addJS('/assets/client-js/client/Client.js', 0, { type: 'module' });
37
+
38
+ this.application.emit('documentCreated', this);
39
+ }
40
+
41
+
42
+ // HTTP2 push, Link headers
43
+ push(response: ServerResponse): void {
44
+ const resourcesJS = this.head.js.map((resource) => {
45
+ return `<${resource.path}>; rel=${this.application.config.http.linkHeaderRel}; as=script; crossorigin=anonymous`;
46
+ });
47
+ const resourcesCSS = this.head.css.map((resource) => {
48
+ return `<${resource.path}>; rel=${this.application.config.http.linkHeaderRel}; as=style; crossorigin=anonymous`;
49
+ });
50
+ const value = resourcesCSS.concat(resourcesJS).join(', ');
51
+ response.setHeader('Link', value);
52
+ }
53
+
54
+ body(): string {
55
+ return this.dom.innerHTML + '\n' + this.appendHTML;
56
+ }
57
+
58
+ public initInitializers(): Record<string, string> {
59
+ const initializers: {
60
+ [key: string] : string
61
+ } = {};
62
+
63
+ for (const name in this.initializers) {
64
+ initializers[name] = this.initializers[name].toString();
65
+ }
66
+
67
+ const initializersString = '<script type="application/javascript">window.initializers = ' + JSON.stringify(initializers) + '</script>';
68
+
69
+ this.head.add(initializersString);
70
+ this.initializersInitialized = true;
71
+ return initializers;
72
+ }
73
+
74
+ private initClientConfig(): void {
75
+ const clientConf: StructuredClientConfig = {
76
+ componentRender: this.application.config.url.componentRender,
77
+ componentNameAttribute: this.application.config.components.componentNameAttribute
78
+ }
79
+ const clientConfString = `<script type="application/javascript">window.structuredClientConfig = ${JSON.stringify(clientConf)}</script>`;
80
+ this.head.add(clientConfString);
81
+ }
82
+
83
+ public toString(): string {
84
+
85
+ if (! this.initializersInitialized) {
86
+ this.initInitializers();
87
+ this.initClientConfig();
88
+ }
89
+
90
+ return `<!DOCTYPE html>
91
+ <html lang="${this.language}">
92
+ ${this.head.toString()}
93
+ <body>
94
+ ${this.body()}
95
+ </body>
96
+ </html>`;
97
+ }
98
+
99
+ // generate an unique component id and store it to componentIds
100
+ // so that each component within the document has an unique id
101
+ allocateId(component: Component): string {
102
+ if (! this.componentIds) {
103
+ // if auto initialized it may have not yet initialized it as an empty array
104
+ this.componentIds = [];
105
+ }
106
+
107
+ // if component has data-id then md5(ComponentName:id), otherwise md5(ComponentName:DOM path:attributes JSON string)
108
+ let id = Md5.hashStr(`${component.name}:${'id' in component.attributes ? component.attributes.id : `${component.path.join('/')}:${JSON.stringify(component.attributesRaw)}`}`);
109
+
110
+ // but multiple components might render the exact same thing
111
+ // so in those cases travel up the tree and append the MD5 sum of the parent
112
+ if (this.componentIds.includes(id)) {
113
+ let current: Component|Document|null = component.parent;
114
+
115
+ do {
116
+ if (current === null || current.isRoot) {
117
+ // reached root without being able to uniquely identify
118
+ // resort to a random string
119
+ // these components won't work as expected
120
+ // they will lose access to their store (client side) whenever they or their parent is redrawn
121
+ console.error(`Could not define an unique ID for component ${component.name}, path: ${component.path}`);
122
+ id = randomString(16);
123
+ } else {
124
+ id += '-' + Md5.hashStr(current.dom.outerHTML);
125
+ }
126
+ current = current?.parent || null;
127
+ } while(this.componentIds.includes(id));
128
+ }
129
+
130
+ this.componentIds.push(id);
131
+
132
+ return id;
133
+ }
134
+
135
+ // load the view from file system
136
+ public async loadView(pathRelative: string, data?: LooseObject): Promise<boolean> {
137
+ const viewPath = path.resolve('../' + this.application.config.components.path + '/' + pathRelative + (pathRelative.endsWith('.html') ? '' : '.html'));
138
+
139
+ if (! existsSync(viewPath)) {
140
+ console.warn(`Couldn't load document ${this.document.head.title}: ${viewPath}`);
141
+ return false;
142
+ }
143
+
144
+ const html = readFileSync(viewPath).toString();
145
+
146
+ await this.init(html, data);
147
+
148
+ return true;
149
+ }
150
+
151
+ // load given component into this document
152
+ public async loadComponent(componentName: string, data?: LooseObject): Promise<void> {
153
+ const componentEntry = this.document.application.components.getByName(componentName);
154
+ if (componentEntry) {
155
+ const dataString = data === undefined ? '' : Object.keys(data).reduce((prev, key) => {
156
+ prev.push(`data-${key}="${attributeValueToString(key, data[key])}"`)
157
+ return prev;
158
+ }, [] as Array<string>).join(' ');
159
+ await this.init(`<${componentName} ${dataString}></${componentName}>`, data);
160
+ }
161
+ }
162
+
163
+ }
@@ -0,0 +1,150 @@
1
+ import { DocumentResource } from '../Types';
2
+
3
+ export class DocumentHead {
4
+
5
+ title: string;
6
+ js: Array<DocumentResource> = [];
7
+ css: Array<DocumentResource> = [];
8
+ custom: Array<string> = [];
9
+ charset = 'UTF-8';
10
+
11
+ favicon: {
12
+ image: string|null,
13
+ type: string
14
+ } = {
15
+ image: null,
16
+ type: 'image/png'
17
+ }
18
+
19
+ constructor(title: string) {
20
+ this.title = title;
21
+ }
22
+
23
+ public setTitle(title: string): void {
24
+ this.title = title;
25
+ }
26
+
27
+ public add(str: string): void {
28
+ this.custom.push(str);
29
+ }
30
+
31
+ public remove(str: string): void {
32
+ this.custom = this.custom.filter((strExisting) => {
33
+ return strExisting !== str;
34
+ });
35
+ }
36
+
37
+ public addJS(path: string, priority: number = 0, attributes: { [ attributeName: string ] : string|null } = {}): DocumentResource {
38
+ const resource = this.toResource(path, priority, attributes);
39
+ this.js.push(resource);
40
+ return resource;
41
+ }
42
+
43
+ public addCSS(path: string, priority: number = 0, attributes: { [ attributeName: string ] : string|null } = {}): DocumentResource {
44
+ const resource = this.toResource(path, priority, attributes);
45
+ this.css.push(resource);
46
+ return resource;
47
+ }
48
+
49
+ public removeJS(path: string): void {
50
+ const index = this.js.findIndex((resource) => {
51
+ return resource.path == path;
52
+ });
53
+ this.js.splice(index, 1);
54
+ }
55
+
56
+ public removeCSS(path: string): void {
57
+ const index = this.css.findIndex((resource) => {
58
+ return resource.path == path;
59
+ });
60
+ this.css.splice(index, 1);
61
+ }
62
+
63
+ private toResource(path: string, priority: number = 0, attributes: { [ attributeName: string ] : string|null } = {}): DocumentResource {
64
+ return {
65
+ path,
66
+ priority,
67
+ attributes
68
+ };
69
+ }
70
+
71
+ private attributesString(resource: DocumentResource): string {
72
+ let attributesString = '';
73
+ for (const attributeName in resource.attributes) {
74
+ const val = resource.attributes[attributeName]
75
+ if (val === null) {
76
+ attributesString += ` ${attributeName}`;
77
+ } else {
78
+ attributesString += ` ${attributeName}="${val}"`;
79
+ }
80
+ }
81
+ return attributesString;
82
+ }
83
+
84
+ public toString(): string {
85
+
86
+ const css = this.css.reduce((prev, curr) => {
87
+ return prev + '\n' + `<link rel="stylesheet" href="${curr.path}"${this.attributesString(curr)}>`;
88
+ }, '');
89
+
90
+ const js = this.js.reduce((prev, curr) => {
91
+ return prev + '\n' + `<script src="${curr.path}"${this.attributesString(curr)}></script>`;
92
+ }, '');
93
+
94
+ const custom = this.custom.reduce((prev, curr) => {
95
+ return prev + '\n' + curr;
96
+ }, '');
97
+
98
+ return `<head>
99
+ <meta charset="${this.charset}">
100
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
101
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
102
+ <title>${this.title}</title>
103
+ <link rel="icon" type="${this.favicon.type}" href="${this.favicon.image}">
104
+ ${css}
105
+ ${js}
106
+ ${custom}
107
+ </head>`;
108
+
109
+ }
110
+
111
+ public setFavicon(faviconPath: string|{
112
+ image: string|null,
113
+ type: string
114
+ }): void {
115
+ if (typeof faviconPath === 'string') {
116
+ this.favicon = {
117
+ image: faviconPath,
118
+ type: this.faviconType(faviconPath)
119
+ }
120
+ return;
121
+ }
122
+ // favicon given as object
123
+ if (faviconPath.type === '') {
124
+ // detect type
125
+ faviconPath.type = faviconPath.image ? this.faviconType(faviconPath.image) : 'image/png';
126
+ }
127
+ this.favicon = faviconPath;
128
+ }
129
+
130
+ private faviconType(file: string): string {
131
+ let ext: RegExpExecArray|string|null = /\.([^.]+)$/.exec(file);
132
+ let type = 'image/png';
133
+ if (ext !== null) {
134
+ ext = ext[1].toLowerCase();
135
+ const types: {
136
+ [key: string] : string
137
+ } = {
138
+ 'png' : 'image/png',
139
+ 'jpg' : 'image/jpeg',
140
+ 'jpeg' : 'image/jpeg',
141
+ 'gif' : 'image/gif',
142
+ 'ico' : 'image/x-icon'
143
+ }
144
+ if (types[ext]) {
145
+ type = types[ext];
146
+ }
147
+ }
148
+ return type;
149
+ }
150
+ }
@@ -0,0 +1,231 @@
1
+ import { FormValidationEntry, PostedDataDecoded, ValidationErrors, ValidationErrorsSingle, ValidationResult, ValidationRuleWithArguments, ValidatorErrorDecorator, ValidatorFunction } from '../Types';
2
+
3
+ export class FormValidation {
4
+
5
+ fieldRules: Array<FormValidationEntry> = [];
6
+
7
+ // if true, only a single error is kept per field
8
+ singleError: boolean = false;
9
+
10
+ validators: {
11
+ [name: string] : ValidatorFunction
12
+ } = {
13
+ 'required' : async (data, field) => {
14
+ if (! (field in data)) {
15
+ // field missing but required
16
+ return false;
17
+ }
18
+
19
+ const value = data[field];
20
+ if (typeof value !== 'string') {return false;}
21
+
22
+ // field exists, but consider empty strings non valid
23
+ return value.trim().length > 0;
24
+ },
25
+ 'number' : async (data, field) => {
26
+ // does not need to be a number, but rather contain only numbers
27
+ // eg. 14
28
+ const value = data[field];
29
+ if (typeof value !== 'string') {return false;}
30
+ return /^-?\d+$/.test(value);
31
+ },
32
+ 'float' : async (data, field) => {
33
+ // 14.2
34
+ const value = data[field];
35
+ if (typeof value !== 'string') {return false;}
36
+ return /^-?\d+\.\d+$/.test(value);
37
+ },
38
+ 'numeric' : async (data, field, arg, rules) => {
39
+ // 14 or 14.2
40
+ return await this.validators['number'](data, field, arg, rules) || await this.validators['float'](data, field, arg, rules);
41
+ },
42
+ 'min' : async(data, field, arg, rules) => {
43
+ const value = data[field];
44
+ if (typeof value !== 'string') {return false;}
45
+ if (await this.validators['numeric'](data, field, arg, rules)) {
46
+ return parseFloat(value) >= arg;
47
+ }
48
+ // non numeric value, can't be determined so consider invalid
49
+ return false;
50
+ },
51
+ 'max' : async(data, field, arg, rules) => {
52
+ const value = data[field];
53
+ if (typeof value !== 'string') {return false;}
54
+ if (await this.validators['numeric'](data, field, arg, rules)) {
55
+ return parseFloat(value) <= arg;
56
+ }
57
+ // non numeric value, can't be determined so consider invalid
58
+ return false;
59
+ },
60
+ 'minLength' : async (data, field, arg) => {
61
+ const value = data[field];
62
+ if (typeof value !== 'string') {return false;}
63
+ return value.length >= arg;
64
+ },
65
+ 'maxLength' : async (data, field, arg) => {
66
+ const value = data[field];
67
+ if (typeof value !== 'string') {return false;}
68
+ return value.length <= arg;
69
+ },
70
+ 'alphanumeric' : async (data, field) => {
71
+ const value = data[field];
72
+ if (typeof value !== 'string') {return false;}
73
+ // string must contain only letters and numbers
74
+ return /^[a-zA-Z0-9]+$/.test(value);
75
+ },
76
+ 'validEmail' : async (data, field) => {
77
+ const value = data[field];
78
+ if (typeof value !== 'string') {return false;}
79
+ return /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test(value);
80
+ }
81
+ }
82
+
83
+ // functions that return error messages
84
+ decorators: {
85
+ [validatorName: string] : ValidatorErrorDecorator
86
+ } = {
87
+ 'required' : (fieldHuman) => {
88
+ return `${fieldHuman} is required`;
89
+ },
90
+ 'number' : (fieldHuman) => {
91
+ return `${fieldHuman} has to be a whole number`;
92
+ },
93
+ 'float' : (fieldHuman) => {
94
+ return `${fieldHuman} has to be a decimal number`;
95
+ },
96
+ 'numeric' : (fieldHuman) => {
97
+ return `${fieldHuman} has to contain a numeric value`;
98
+ },
99
+ 'min' : (fieldHuman, data, field, arg) => {
100
+ return `${fieldHuman} has to be a value greater than ${arg}`;
101
+ },
102
+ 'max' : (fieldHuman, data, field, arg) => {
103
+ return `${fieldHuman} has to be a value lower than ${arg}`;
104
+ },
105
+ 'minLength' : (fieldHuman, data, field, arg) => {
106
+ return `${fieldHuman} has to contain at least ${arg} characters`;
107
+ },
108
+ 'maxLength' : (fieldHuman, data, field, arg) => {
109
+ return `${fieldHuman} has to contain no more than ${arg} characters`;
110
+ },
111
+ 'alphanumeric' : (fieldHuman) => {
112
+ return `${fieldHuman} can contain only letter and numbers`;
113
+ },
114
+ 'validEmail' : () => {
115
+ return `Please enter a valid email address`;
116
+ }
117
+ }
118
+
119
+ public addRule(fieldName: string, nameHumanReadable: string, rules: Array<string|ValidationRuleWithArguments|ValidatorFunction>): void {
120
+ const rule: FormValidationEntry = {
121
+ field: [fieldName, nameHumanReadable],
122
+ rules
123
+ }
124
+ this.fieldRules.push(rule);
125
+ }
126
+
127
+ // register new/override existing validator
128
+ public registerValidator(name: string, validator: ValidatorFunction, decorator?: ValidatorErrorDecorator): void {
129
+ this.validators[name] = validator;
130
+
131
+ // if decorator is provided, store it
132
+ if (typeof decorator === 'function') {
133
+ this.decorators[name] = decorator;
134
+ }
135
+ }
136
+
137
+ public registerDecorator(name: string, decorator: ValidatorErrorDecorator): void {
138
+ this.decorators[name] = decorator;
139
+ }
140
+
141
+ public async validate(data: PostedDataDecoded): Promise<ValidationResult> {
142
+
143
+ const result: ValidationResult = {
144
+ valid: true,
145
+ errors: {}
146
+ }
147
+
148
+ // run all validation rules
149
+ for (let i = 0; i < this.fieldRules.length; i++) {
150
+ const entry = this.fieldRules[i];
151
+
152
+ const isRequired = entry.rules.includes('required');
153
+
154
+ const value = data[entry.field[0]];
155
+ const possiblyValidDataExists = typeof value === 'string';
156
+
157
+ // content - a non required field that is not passed or is blank
158
+ // will pass all checks, for example rules ['numeric']
159
+ // we expect the field to contain a numeric value, but we don't expect the field to exist in the first place
160
+ // se all validators are skipped for content entries
161
+ const isContent = ! isRequired && (! possiblyValidDataExists || value.trim().length === 0);
162
+
163
+ if (! isContent) {
164
+ for (let j = 0; j < entry.rules.length; j++) {
165
+ const rule = entry.rules[j];
166
+
167
+ if (typeof rule === 'function') {
168
+ // custom callback (ValidatorFunction)
169
+ const valid = await rule.apply(this, [data, entry.field[0], 0, entry.rules]);
170
+ if (! valid) {
171
+ await this.addError(result.errors, data, entry.field, 'callback');
172
+ }
173
+ } else {
174
+ // uses a validator
175
+ if (typeof rule === 'string') {
176
+ // no arguments
177
+ if (this.validators[rule]) {
178
+ const valid = await this.validators[rule].apply(this, [data, entry.field[0], 0, entry.rules]);
179
+ if (! valid) {
180
+ await this.addError(result.errors, data, entry.field, rule);
181
+ }
182
+ }
183
+ } else {
184
+ // rule with arguments
185
+ const validatorName = rule[0];
186
+ const arg = rule[1];
187
+ if (this.validators[validatorName]) {
188
+ const valid = await this.validators[validatorName].apply(this, [data, entry.field[0], arg, entry.rules]);
189
+ if (! valid) {
190
+ await this.addError(result.errors, data, entry.field, validatorName, arg);
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ }
197
+ }
198
+
199
+ }
200
+
201
+ // valid if no errors
202
+ result.valid = Object.keys(result.errors).length == 0;
203
+
204
+ return result;
205
+ }
206
+
207
+ private async addError(errors: ValidationErrors|ValidationErrorsSingle, data: PostedDataDecoded, field: [string, string], rule: string, arg?: any): Promise<void> {
208
+ // error will be a human readable error returned by decorator
209
+ // if no decorator is found for the rule, rule itself becomes the error
210
+ let errorMessage = '';
211
+ if (this.decorators[rule]) {
212
+ errorMessage = await this.decorators[rule](field[1], data, field[0], arg);
213
+ } else {
214
+ errorMessage = rule;
215
+ }
216
+
217
+ if (! this.singleError) {
218
+ if (! errors[field[0]]) {
219
+ errors[field[0]] = [];
220
+ }
221
+
222
+ (errors as ValidationErrors)[field[0]].push(errorMessage);
223
+ } else {
224
+ // single error mode
225
+ if (! errors[field[0]]) {
226
+ errors[field[0]] = errorMessage;
227
+ }
228
+ }
229
+ }
230
+
231
+ }
@@ -0,0 +1,51 @@
1
+ import { HelperDelegate } from "handlebars";
2
+ import { default as HandlebarsInstance } from 'handlebars';
3
+ import { LooseObject } from "../Types.js";
4
+
5
+ // handlebars helper manager
6
+ export class Handlebars {
7
+
8
+ readonly instance: typeof HandlebarsInstance = HandlebarsInstance;
9
+ readonly helpers: Record<string, HelperDelegate> = {};
10
+
11
+ // register a handlebars helper
12
+ // registers it to this.instance and stores it to this.helpers
13
+ // if needed later all helpers can be applied to another instance of handlebars
14
+ public register(name: string, helper: HelperDelegate): void {
15
+ this.helpers[name] = helper;
16
+ this.instance.registerHelper(name, helper);
17
+ }
18
+
19
+ // load helpers from given path and register them
20
+ public async loadHelpers(path: string): Promise<void> {
21
+ try {
22
+ const helpers = await import(path) as {
23
+ default?: Record<string, HelperDelegate>
24
+ };
25
+ if (! ('default' in helpers)) {
26
+ throw new Error('File has no default export, expected default: Record<string, HelperDelegate>');
27
+ }
28
+
29
+ // register helpers
30
+ for (const name in helpers.default) {
31
+ this.register(name, helpers.default[name]);
32
+ }
33
+ } catch(e) {
34
+ throw new Error(e.message);
35
+ }
36
+ }
37
+
38
+ // apply all registered helpers to given Handlebars instance
39
+ public applyTo(handlebarsInstance: typeof HandlebarsInstance): void {
40
+ for (const name in this.helpers) {
41
+ handlebarsInstance.registerHelper(name, this.helpers[name]);
42
+ }
43
+ }
44
+
45
+ // given a HTML template that uses handlebars synthax and data
46
+ // compile and return resulting HTML
47
+ public compile(html: string, data: LooseObject): string {
48
+ const template = this.instance.compile(html);
49
+ return template(data);
50
+ }
51
+ }