5htp-core 0.2.0 → 0.2.1-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 (46) hide show
  1. package/package.json +3 -3
  2. package/src/client/app/component.tsx +0 -3
  3. package/src/client/assets/css/components.less +52 -0
  4. package/src/client/assets/css/core.less +7 -28
  5. package/src/client/assets/css/theme.less +1 -1
  6. package/src/client/assets/css/{borders.less → utils/borders.less} +0 -0
  7. package/src/client/assets/css/{layouts.less → utils/layouts.less} +0 -0
  8. package/src/client/assets/css/{medias.less → utils/medias.less} +0 -1
  9. package/src/client/assets/css/{sizing.less → utils/sizing.less} +0 -0
  10. package/src/client/assets/css/{spacing.less → utils/spacing.less} +0 -0
  11. package/src/client/components/Card/index.tsx +11 -5
  12. package/src/client/components/Dialog/Manager.tsx +3 -3
  13. package/src/client/components/Dialog/index.less +2 -4
  14. package/src/client/components/Row/index.less +0 -2
  15. package/src/client/components/Table/index.tsx +3 -2
  16. package/src/client/components/containers/champs.less +0 -2
  17. package/src/client/components/index.ts +16 -1
  18. package/src/client/components/input/BaseV2/index.less +0 -2
  19. package/src/client/components/input/Date/index.less +0 -2
  20. package/src/client/components/input/Periode/index.less +0 -2
  21. package/src/client/components/input/Radio/index.less +0 -2
  22. package/src/client/components/input/UploadImage/index.less +0 -2
  23. package/src/client/services/router/components/Page.tsx +4 -4
  24. package/src/client/services/router/components/router.tsx +11 -2
  25. package/src/client/services/router/index.tsx +11 -6
  26. package/src/client/services/router/request/api.ts +22 -24
  27. package/src/client/services/router/response/index.tsx +1 -1
  28. package/src/client/services/router/response/page.ts +9 -14
  29. package/src/common/router/request/api.ts +1 -1
  30. package/src/common/router/response/page.ts +9 -1
  31. package/src/common/validation/schema.ts +1 -0
  32. package/src/common/validation/validator.ts +13 -6
  33. package/src/server/app/index.ts +2 -1
  34. package/src/server/services/console/bugReporter.ts +1 -1
  35. package/src/server/services/database/connection.ts +12 -10
  36. package/src/server/{error/index.ts → services/database/debug.ts} +7 -0
  37. package/src/server/services/email/index.ts +13 -21
  38. package/src/server/services/email/transporter.ts +38 -0
  39. package/src/server/services/router/index.ts +8 -7
  40. package/src/server/services/router/request/api.ts +9 -6
  41. package/src/server/services/router/response/index.ts +8 -3
  42. package/src/server/services/users/index.ts +1 -1
  43. package/src/server/{data → services_old}/SocketClient.ts +0 -0
  44. package/src/server/{data/Token.olg.ts → services_old/Token.old.ts} +0 -0
  45. package/src/server/{data → services_old}/aes.ts +0 -0
  46. package/src/client/assets/css/components/components.less +0 -31
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
- "description": "Convenient Full Stack TypeScript framework designed for Performance and Productivity.",
4
- "version": "0.2.0",
3
+ "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
+ "version": "0.2.1-2",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -84,6 +84,6 @@
84
84
  "@types/universal-analytics": "^0.4.5",
85
85
  "@types/webpack-env": "^1.16.2",
86
86
  "@types/ws": "^7.4.7",
87
- "babel-plugin-glob-import": "^0.0.3"
87
+ "babel-plugin-glob-import": "^0.0.6-2"
88
88
  }
89
89
  }
@@ -14,9 +14,6 @@ import DialogManager from '@client/components/Dialog/Manager'
14
14
  import Router from '@client/services/router/components/router';
15
15
  import type { TClientOrServerContext } from '@common/router';
16
16
 
17
- // Resources
18
- import "@client/assets/css/core.less";
19
-
20
17
  /*----------------------------------
21
18
  - COMPOSANT
22
19
  ----------------------------------*/
@@ -0,0 +1,52 @@
1
+ // Text
2
+ @import './text/text.less';
3
+ @import './text/icons.less';
4
+ @import './text/icons.less';
5
+ @import './text/titres.less';
6
+
7
+ // Components
8
+ @import './components/input.less';
9
+ @import './components/button.less';
10
+ @import './components/lists.less';
11
+ @import './components/card.less';
12
+ @import './components/logo.less';
13
+ @import './components/table.less';
14
+ @import './components/other.less';
15
+ @import '@client/components/chart/chart.less';
16
+ @import '@client/components/data/progressbar/index.less';
17
+
18
+ // Les classes utilitaires override les classes de composant
19
+ @import './utils/borders.less';
20
+ @import './utils/layouts.less';
21
+
22
+ .white-card() {
23
+ background: white;
24
+ box-shadow: 0 3px 2px fade(#000, 10%);
25
+ border: solid 1px fade(#000, 10%);
26
+ border-radius: @radius;
27
+ }
28
+
29
+ .card,
30
+ .input.text,
31
+ .btn,
32
+ .table,
33
+ i.solid {
34
+
35
+ .build-theme-bg( #fff, #8E8E8E);
36
+
37
+ &:not(.bg) {
38
+ .white-card();
39
+ }
40
+
41
+ .bg & {
42
+ box-shadow: none;
43
+ border: none;
44
+ }
45
+ }
46
+
47
+ //.card.clickable:hover,
48
+ .btn:hover,
49
+ .input.text.focus {
50
+ z-index: 5;
51
+ box-shadow: 0 10px 50px fade(#000, 15%);
52
+ }
@@ -1,35 +1,14 @@
1
+ // Utils
2
+ @import './utils/medias.less';
3
+ @import './utils/sizing.less';
4
+ @import './utils/spacing.less';
5
+ @import (reference) "./theme.less";
6
+
7
+ // Fonts
1
8
  @import '../fonts/Inter/index.less';
2
9
  @import '../fonts/Rubik/index.less';
3
10
  @import '../fonts/Lato/index.less';
4
11
 
5
- @import './text/text.less';
6
- @import './text/icons.less';
7
- @import './text/icons.less';
8
- @import './text/titres.less';
9
-
10
- @import './components/components.less';
11
- @import './components/input.less';
12
- @import './components/button.less';
13
- @import './components/lists.less';
14
- @import './components/card.less';
15
- @import './components/logo.less';
16
- @import './components/table.less';
17
- @import './components/other.less';
18
-
19
- // Les classes utilitaires override les classes de composant
20
- @import './borders.less';
21
- @import './spacing.less';
22
- @import './layouts.less';
23
-
24
- @import './medias.less';
25
- @import './sizing.less';
26
-
27
- @import '@client/components/chart/chart.less';
28
- @import '@client/components/data/progressbar/index.less';
29
-
30
- @import (reference) "./theme.less";
31
- @import "~@/client/assets/theme.less";
32
-
33
12
  // Apply the theme class
34
13
  .bg {
35
14
  background: var(--cBg);
@@ -17,7 +17,7 @@
17
17
  // Background
18
18
  // TODO: Nettoyer
19
19
  @bg2: darken(@bg, 8%);
20
- @bgDark: darken(@bg, 5%);
20
+ @bgDark: darken(@bg, 7%);
21
21
  @bgDarkPlus: darken(@bg, 10%);
22
22
  --cBg: @bg;
23
23
  --cBg2: @bg2;
@@ -1,4 +1,3 @@
1
- @import (reference) "~@/client/assets/theme.less";
2
1
 
3
2
  img.img,
4
3
  .bg.img {
@@ -7,7 +7,7 @@ import React from 'react';
7
7
  import type { ComponentChild } from 'preact';
8
8
 
9
9
  // Core components
10
- import Button from '@client/components/button';
10
+ import { Logo } from '@client/components';
11
11
  import { Link } from '@client/services/router';
12
12
 
13
13
  // Resources
@@ -28,6 +28,7 @@ export type Props = {
28
28
  link?: string,
29
29
  cover?: {
30
30
  color?: string,
31
+ image?: string,
31
32
  title?: string,
32
33
  logo?: ComponentChild
33
34
  },
@@ -49,10 +50,15 @@ export default ({ title, link, cover, metas, class: className = '' }: Props) =>
49
50
 
50
51
  {cover && (
51
52
  <header class="bg img row al-left cover pdb-1" style={{
52
- backgroundColor: cover.color
53
+ backgroundColor: cover.color,
54
+ backgroundImage: cover.image
55
+ ? 'url(' + cover.image + ')'
56
+ : undefined
53
57
  }}>
54
58
 
55
- {cover?.logo}
59
+ {typeof cover.logo === 'string'
60
+ ? <Logo src={cover.logo} size="xl" />
61
+ : cover.logo}
56
62
 
57
63
  {cover.title && (
58
64
  <strong>
@@ -72,9 +78,9 @@ export default ({ title, link, cover, metas, class: className = '' }: Props) =>
72
78
  )}
73
79
 
74
80
  {metas && (
75
- <ul class="row fill">
81
+ <ul class="row fill al-top">
76
82
  {metas.map(({ label, value, class: className }) => (
77
- <li class={"col al-left sp-05"}>
83
+ <li class={"col al-left txt-left sp-05"}>
78
84
  {label}
79
85
  <strong class={className}>{value}</strong>
80
86
  </li>
@@ -163,9 +163,9 @@ export const createDialog = (app: Application, isToast: boolean): DialogActions
163
163
  setToasts: undefined as unknown as DialogActions["setToasts"],
164
164
 
165
165
  confirm: (title: string, content: string | ComponentChild, defaultBtn: 'Yes'|'No' = 'No') => show<boolean>(({ close }) => (
166
- <div class="col">
166
+ <div class="card col">
167
167
  <header>
168
- <h1>{title}</h1>
168
+ <h2>{title}</h2>
169
169
  </header>
170
170
  {typeof content === 'string' ? <p>{content}</p> : content}
171
171
  <footer class="row fill">
@@ -181,7 +181,7 @@ export const createDialog = (app: Application, isToast: boolean): DialogActions
181
181
  </div>
182
182
  )),
183
183
 
184
- loading: (title: string) => app.loadIndicator = show({
184
+ loading: (title: string) => app.loading = show({
185
185
  title: title,
186
186
  type: 'loading'
187
187
  }),
@@ -1,5 +1,3 @@
1
- @import (reference) "~@/client/assets/theme.less";
2
-
3
1
  @toast-zindex: 999;
4
2
 
5
3
  #dialog {
@@ -89,10 +87,10 @@
89
87
  border-radius: @radius;
90
88
 
91
89
  // Desktop = vertically center the modal
92
- /*@media (min-width: @responsive1) {
90
+ @media (min-width: 900px) {
93
91
  justify-content: center;
94
92
  padding: @spacing;
95
- }*/
93
+ }
96
94
 
97
95
  // Pour les animations (ex: conffetis
98
96
  > canvas {
@@ -1,5 +1,3 @@
1
- @import (reference) "~@/client/assets/theme.less";
2
-
3
1
  @hideeWidth: 10em;
4
2
 
5
3
  .scrollable-row {
@@ -1,3 +1,4 @@
1
+
1
2
  /*----------------------------------
2
3
  - DEPENDANCES
3
4
  ----------------------------------*/
@@ -22,7 +23,7 @@ export type Props<TRow> = {
22
23
  columns: (row: TRow, rows: TRow[], index: number) => TColumn[];
23
24
 
24
25
  setData?: (rows: TRow[]) => void,
25
- vide?: ComponentChild,
26
+ empty?: ComponentChild,
26
27
  className?: string,
27
28
 
28
29
  actions?: TAction<TRow>[]
@@ -39,7 +40,7 @@ export type TColumn = {
39
40
  - COMPOSANTS
40
41
  ----------------------------------*/
41
42
  export default function Liste<TRow extends TDonneeInconnue>({
42
- data: rows, setData, vide ,
43
+ data: rows, setData, empty ,
43
44
  columns, actions, ...props
44
45
  }: Props<TRow>) {
45
46
 
@@ -1,5 +1,3 @@
1
- @import '~@/general/client/assets/css/vars.less';
2
-
3
1
  @plugin "~@client/components/_base/plugin-less.js";
4
2
 
5
3
  /*----------------------------------
@@ -5,4 +5,19 @@ export { default as Table } from './Table';
5
5
  export { default as Select } from './Select';
6
6
  export { default as Amount } from './Amount';
7
7
  export { default as Logo } from './Logo';
8
- export { default as Input } from './input';
8
+
9
+ export { default as Input } from './input';
10
+ export { default as Textarea } from './input/Textarea';
11
+ export { default as Number } from './input/Number';
12
+ export { default as Slider } from './input/Slider';
13
+ export { default as Upload } from './input/Upload';
14
+ export { default as Radio } from './input/Radio';
15
+
16
+ // TOD: fix popover component
17
+ //export { default as Date } from './input/Date';
18
+ //export { default as Periode } from './input/Periode';
19
+
20
+ // TODO: adapt
21
+ //export { default as Couleur } from './input/Couleur';
22
+ //export { default as Code } from './input/Code';
23
+ //export { default as Rte } from './input/Rte';
@@ -1,5 +1,3 @@
1
- @import (reference) '~@/client/assets/theme.less';
2
-
3
1
  @hInput: @sizeComponent;
4
2
 
5
3
  // TODO: Adapter textarea à input/basev2
@@ -1,5 +1,3 @@
1
- @import (reference) '~@/general/client/assets/css/vars.less';
2
-
3
1
  .champ.periode {
4
2
 
5
3
  display: flex;
@@ -1,5 +1,3 @@
1
- @import (reference) '~@/general/client/assets/css/vars.less';
2
-
3
1
  .champ.periode {
4
2
 
5
3
  display: flex;
@@ -1,5 +1,3 @@
1
- @import '~@/general/client/assets/css/vars.less';
2
-
3
1
  @hRadio: 40px;
4
2
  @margeRadio: 5px;
5
3
 
@@ -1,5 +1,3 @@
1
- @import (reference) '~@/client/assets/theme.less';
2
-
3
1
  .uploadImg {
4
2
  position: relative;
5
3
  overflow: hidden;
@@ -19,20 +19,20 @@ export default ({ page, isCurrent }: { page: Page, isCurrent?: boolean }) => {
19
19
  const context = useContext();
20
20
 
21
21
  const [apiData, setApiData] = React.useState<{[k: string]: any} | null>(
22
- page.loadIndicator ? null : page.data
22
+ page.loading ? null : page.data
23
23
  );
24
24
  page.setAllData = setApiData;
25
25
 
26
26
  React.useEffect(() => {
27
27
 
28
28
  // Fetch the data asynchronously for the first time
29
- if (apiData === null && isCurrent)
29
+ if (/*apiData === null && */isCurrent)
30
30
  page.fetchData().then( loadedData => {
31
- page.loadIndicator = false;
31
+ page.loading = false;
32
32
  setApiData(loadedData);
33
33
  })
34
34
 
35
- }, []);
35
+ }, [page]);
36
36
 
37
37
  return (
38
38
  <div
@@ -62,8 +62,9 @@ export default ({ service: router }: { service: Router }) => {
62
62
  return;
63
63
  }
64
64
 
65
- // Set.loadIndicator state
66
- newpage.loadIndicator = <i src="spin" />
65
+ // Set.loading state
66
+ newpage.isLoading = true;
67
+ newpage.loading = <i src="spin" />
67
68
  // Add page container
68
69
  setPages( pages => {
69
70
 
@@ -79,7 +80,15 @@ export default ({ service: router }: { service: Router }) => {
79
80
  const curLayout = currentRoute?.options.layout;
80
81
  const newLayout = newpage?.route.options.layout;
81
82
  if (newLayout && curLayout && newLayout.path !== curLayout.path) {
83
+
84
+ // TEMPORARY FIX: reload everything when we change layout
85
+ // Because layout can have a different theme
86
+ // But when we call setLayout, the style of the previous layout are still oaded and applied
87
+ // Find a way to unload the previous layout / page resources before to load the new one
82
88
  console.log(LogPrefix, `Changing layout. Before:`, curLayout, 'New layout:', newLayout);
89
+ window.location.replace(request.path);
90
+ return pages;
91
+
83
92
  context.app.setLayout(newLayout);
84
93
  }
85
94
 
@@ -343,6 +343,7 @@ export default class ClientRouter<
343
343
  const request = new ClientRequest(location, this);
344
344
 
345
345
  // Restituate SSR response
346
+ let apiData: {} = {}
346
347
  if (this.ssrData) {
347
348
 
348
349
  console.log("SSR Response restitution ...");
@@ -350,11 +351,15 @@ export default class ClientRouter<
350
351
  request.user = this.ssrData.user || null;
351
352
 
352
353
  request.data = this.ssrData.request.data;
354
+
355
+ apiData = this.ssrData.page.data || {};
353
356
  }
354
357
 
355
- const response = await this.createResponse(route, request)
358
+ // Replacer api data par ssr data
359
+
360
+ const response = await this.createResponse(route, request, apiData)
356
361
 
357
- ReactDOM.hydrate(<App context={response.context} />, document.body, () => {
362
+ ReactDOM.hydrate( <App context={response.context} />, document.body, () => {
358
363
 
359
364
  console.log(`Render complete`);
360
365
 
@@ -364,7 +369,7 @@ export default class ClientRouter<
364
369
  private async createResponse(
365
370
  route: TUnresolvedRoute | TRoute,
366
371
  request: ClientRequest<this>,
367
- additionnalData: {} = {}
372
+ pageData: {} = {}
368
373
  ): Promise<ClientPage> {
369
374
 
370
375
  // Load if not done before
@@ -376,7 +381,7 @@ export default class ClientRouter<
376
381
  try {
377
382
 
378
383
  const response = new ClientResponse<this, ClientPage>(request, route);
379
- return await response.runController(additionnalData);
384
+ return await response.runController(pageData);
380
385
 
381
386
  } catch (error) {
382
387
 
@@ -387,7 +392,7 @@ export default class ClientRouter<
387
392
  private async createErrorResponse(
388
393
  e: any,
389
394
  request: ClientRequest<this>,
390
- additionnalData: {} = {}
395
+ pageData: {} = {}
391
396
  ): Promise<ClientPage> {
392
397
 
393
398
  const code = 'http' in e ? e.http : 500;
@@ -406,7 +411,7 @@ export default class ClientRouter<
406
411
  route = this.errors[code] = await this.load(route);
407
412
 
408
413
  const response = new ClientResponse<this, ClientPage>(request, route);
409
- return await response.runController(additionnalData);
414
+ return await response.runController(pageData);
410
415
  }
411
416
 
412
417
  /*----------------------------------
@@ -127,35 +127,33 @@ export default class ApiClient implements ApiClientService {
127
127
  })
128
128
  }
129
129
 
130
- public async fetchSync(fetchers: TFetcherList): Promise<TObjetDonnees> {
131
-
132
- const ids = Object.keys(fetchers);
133
- if (ids.length === 1) {
134
- const id = ids[0];
135
- const fetcher = fetchers[id];
136
- return {
137
- [id]: await this.fetch( fetcher.method, fetcher.path, fetcher.data, undefined )
138
- };
139
- }
140
-
141
- const fetchersArgs: {[id: string]: TFetcherArgs} = {};
142
- for (const id in fetchers)
143
- fetchersArgs[id] = [
144
- fetchers[id].method,
145
- fetchers[id].path,
146
- fetchers[id].data,
147
- ]
130
+ public async fetchSync(fetchers: TFetcherList, alreadyLoadedData: {}): Promise<TObjetDonnees> {
131
+
132
+ // Pick the fetchers where the data is needed
133
+ const fetchersToRun: TFetcherList = {};
134
+ let fetchersCount: number = 0;
135
+ for (const fetcherId in fetchers)
136
+ if (!( fetcherId in alreadyLoadedData )) {
137
+ fetchersToRun[ fetcherId ] = fetchers[ fetcherId ]
138
+ fetchersCount++;
139
+ }
148
140
 
149
- return await this.fetch("POST", "/api", { fetchers: fetchersArgs }, undefined).then((res) => {
141
+ // Fetch all the api data thanks to one http request
142
+ const fetchedData = fetchersCount === 0
143
+ ? 0
144
+ : await this.fetch("POST", "/api", {
145
+ fetchers: fetchersToRun
146
+ }).then((res) => {
150
147
 
151
- const data: TObjetDonnees = {};
152
- for (const id in res)
153
- data[id] = res[id];
148
+ const data: TObjetDonnees = {};
149
+ for (const id in res)
150
+ data[id] = res[id];
154
151
 
155
- return data;
152
+ return data;
156
153
 
157
- });
154
+ });
158
155
 
156
+ return { ...alreadyLoadedData, ...fetchedData }
159
157
  }
160
158
 
161
159
  public configure = (...[method, path, data, options]: TFetcherArgs): AxiosRequestConfig => {
@@ -94,7 +94,7 @@ export default class ClientPageResponse<
94
94
 
95
95
  // Default data type for `return <raw data>`
96
96
  if (result instanceof ClientPage)
97
- await result.render();
97
+ await result.preRender(additionnalData);
98
98
  else
99
99
  throw new Error(`Unsupported response format: ${result.constructor?.name}`);
100
100
 
@@ -2,6 +2,9 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
+ // Npm
6
+ import type { ComponentChild } from 'preact';
7
+
5
8
  // Core
6
9
  import type { TClientOrServerContext } from '@common/router';
7
10
  import PageResponse, { TDataProvider, TFrontRenderer } from "@common/router/response/page";
@@ -22,6 +25,7 @@ import type ClientRouter from '..';
22
25
  export default class ClientPage<TRouter = ClientRouter> extends PageResponse<TRouter> {
23
26
 
24
27
  public isLoading: boolean = false;
28
+ public loading: false | ComponentChild;
25
29
  public scrollToId: string;
26
30
 
27
31
  public constructor(
@@ -38,23 +42,15 @@ export default class ClientPage<TRouter = ClientRouter> extends PageResponse<TRo
38
42
  this.scrollToId = context.request.hash;
39
43
  }
40
44
 
41
- public async render( data?: TObjetDonnees ) {
45
+ public async preRender( data?: TObjetDonnees ) {
42
46
 
43
47
  // Add the page to the context
44
48
  this.context.page = this;
45
-
46
- // Load the fetchers list to load data if needed
47
- if (this.dataProvider)
48
- this.fetchers = this.dataProvider( this.context );
49
+ this.isLoading = true;
49
50
 
50
51
  // Data succesfully loaded
51
- if (data !== undefined) {
52
- this.isLoading = false;
53
- this.data = data;
54
- }
55
-
56
- // Fetch data
57
- this.data = await this.fetchData();
52
+ this.data = data || await this.fetchData();
53
+ this.isLoading = false;
58
54
 
59
55
  return this;
60
56
  }
@@ -81,8 +77,7 @@ export default class ClientPage<TRouter = ClientRouter> extends PageResponse<TRo
81
77
  }));
82
78
  }
83
79
 
84
- public loadIndicator;
85
- public loading(state: boolean) {
80
+ public setLoading(state: boolean) {
86
81
 
87
82
  if (state === true) {
88
83
  if (!document.body.classList.contains("loading"))
@@ -68,5 +68,5 @@ export default abstract class ApiClient {
68
68
 
69
69
  public abstract createFetcher<TData extends unknown = unknown>(...args: TFetcherArgs): TFetcher<TData>;
70
70
 
71
- public abstract fetchSync(fetchers: TFetcherList): Promise<TObjetDonnees>;
71
+ public abstract fetchSync(fetchers: TFetcherList, alreadyLoadedData: {}): Promise<TObjetDonnees>;
72
72
  }
@@ -75,6 +75,14 @@ export default class PageResponse<TRouter extends ClientOrServerRouter = ClientO
75
75
  }
76
76
 
77
77
  public async fetchData() {
78
- return await this.context.request.api.fetchSync( this.fetchers );
78
+
79
+ // Load the fetchers list to load data if needed
80
+ if (this.dataProvider)
81
+ this.fetchers = this.dataProvider({ ...this.context, ...this.context.request.data });
82
+
83
+ // Execute the fetchers for missing data
84
+ console.log(`[router][page] Fetching api data:` + Object.keys(this.fetchers));
85
+ this.data = await this.context.request.api.fetchSync( this.fetchers, this.data );
86
+ return this.data;
79
87
  }
80
88
  }
@@ -28,6 +28,7 @@ export type TValidationResult<TFields extends TSchemaFields> = {
28
28
  }
29
29
 
30
30
  export type TValidatedData<TFields extends TSchemaFields> = {
31
+ // For each field, the values returned by validator.validate()
31
32
  [name in keyof TFields]: ReturnType<TFields[name]["validate"]>
32
33
  }
33
34
 
@@ -46,7 +46,14 @@ type TValidationArgs<TValue, TAllValues extends {}> = [
46
46
 
47
47
  type TValidationFunction<TValue, TAllValues extends {} = {}> = (
48
48
  ...args: TValidationArgs<TValue, TAllValues>
49
- ) => TValue | typeof EXCLUDE_VALUE | undefined;
49
+ ) => TValue | typeof EXCLUDE_VALUE;
50
+
51
+ type TValidateReturnType<
52
+ TOptions extends TValidator<TValue>,
53
+ TValue extends any
54
+ > = TOptions extends { opt: true }
55
+ ? (undefined | TValue)
56
+ : TValue
50
57
 
51
58
  /*----------------------------------
52
59
  - CONST
@@ -57,32 +64,32 @@ export const EXCLUDE_VALUE = "action:exclure" as const;
57
64
  /*----------------------------------
58
65
  - CLASS
59
66
  ----------------------------------*/
60
- export default class Validator<TValue> {
67
+ export default class Validator<TValue, TOptions extends TValidator<TValue> = TValidator<TValue>> {
61
68
 
62
69
  public constructor(
63
70
  public type: string,
64
71
  public validateType: TValidationFunction<TValue>,
65
- public options: TValidator<TValue>
72
+ public options: TOptions
66
73
  ) {
67
74
 
68
75
  }
69
76
 
70
77
  public isEmpty = (val: any) => val === undefined || val === '' || val === null
71
78
 
72
- public validate(...[ val, input, output, correct ]: TValidationArgs<TValue, {}>) {
79
+ public validate(...[ val, input, output, correct ]: TValidationArgs<TValue, {}>): TValidateReturnType<TOptions, TValue> {
73
80
 
74
81
  // Required value
75
82
  if (this.isEmpty(val)) {
76
83
  // Optionnel, on skip
77
84
  if (this.options.opt === true)
78
- return undefined;
85
+ return undefined as TValidateReturnType<TOptions, TValue>;
79
86
  // Requis
80
87
  else
81
88
  throw new InputError("Please enter a value");
82
89
  }
83
90
 
84
91
  // Validate type
85
- return this.validateType(val, input, output, correct);
92
+ return this.validateType(val, input, output, correct) as TValidateReturnType<TOptions, TValue>;
86
93
  }
87
94
 
88
95
  }
@@ -16,7 +16,8 @@ import type { default as Router, Request as ServerRequest } from '@server/servic
16
16
  - TYPES
17
17
  ----------------------------------*/
18
18
 
19
- export { default as Service, TPriority } from './service';
19
+ export { default as Service } from './service';
20
+ export type { TPriority } from './service';
20
21
 
21
22
  type Config = {
22
23
 
@@ -6,7 +6,7 @@
6
6
  import { v4 as uuid } from 'uuid';
7
7
 
8
8
  // Core
9
- import { SqlError } from '@server/error';
9
+ import { SqlError } from '@server/services/database/debug';
10
10
  import type Console from '.';
11
11
 
12
12
  // Types
@@ -7,12 +7,12 @@ import mysql from 'mysql2/promise';
7
7
 
8
8
  // Core: general
9
9
  import Application from '@server/app';
10
- import { SqlError } from '@server/error';
11
10
  import Service from '@server/app/service';
12
11
 
13
12
  // Core: specific
13
+ import { SqlError } from './debug';
14
14
  import type Console from '../console';
15
- import MetadataParser, { TDatabasesList, TMetasTable, TColumnTypes } from './metas';
15
+ import MetadataParser, { TDatabasesList, TMetasTable, TColumnTypes, TMetasColonne } from './metas';
16
16
  import { TMySQLTypeName, mysqlToJs, js as jsTypes } from './datatypes';
17
17
  import Bucket from './bucket';
18
18
 
@@ -157,7 +157,7 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
157
157
  return next();
158
158
 
159
159
  // Normal column
160
- let type: TColumnTypes;
160
+ let databaseColumn: TMetasColonne | undefined;
161
161
  if (field.db && field.table && field.name) {
162
162
 
163
163
  const db = this.tables[ field.db ];
@@ -172,15 +172,17 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
172
172
  throw new Error(`Table metadatas for ${field.db}.${field.table} were not loaded.`);
173
173
  }
174
174
 
175
- const column = table.colonnes[field.name];
176
- if (column === undefined) {
177
- console.error("Field infos:", field);
178
- throw new Error(`Column metadatas for ${field.db}.${field.table}.${field.name} were not loaded.`);
179
- }
175
+ databaseColumn = table.colonnes[field.name];
176
+ }
177
+
178
+
179
+ let type: TColumnTypes;
180
+ if (databaseColumn !== undefined) {
180
181
 
181
- type = column.type;
182
+ type = databaseColumn.type;
182
183
 
183
- // Custom column (computed or aliased)
184
+ // If the column name has not been found in the concerned table,
185
+ // We assume it's a computed column
184
186
  } else {
185
187
 
186
188
  const mysqlType = {
@@ -1,3 +1,10 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ /*----------------------------------
6
+ - CLASS
7
+ ----------------------------------*/
1
8
  export class SqlError extends Error {
2
9
 
3
10
  public constructor(
@@ -8,9 +8,10 @@
8
8
 
9
9
  // Core
10
10
  import Application, { Service } from '@server/app';
11
- import { jsonToHtml } from './utils';
12
11
 
13
- const templates = {} as {[template: string]: (data: any) => string}
12
+ // Speciic
13
+ import { jsonToHtml } from './utils';
14
+ import type { Transporter } from './transporter';
14
15
 
15
16
  /*----------------------------------
16
17
  - SERVICE CONFIG
@@ -22,7 +23,9 @@ export type Config = {
22
23
  transporter: string,
23
24
  from: string
24
25
  },
25
- transporters: Config.EmailTransporters,
26
+ transporters: {
27
+ [transporterName: string]: Transporter
28
+ },
26
29
  bugReport: {
27
30
  from: string,
28
31
  to: string
@@ -33,16 +36,12 @@ export type Hooks = {
33
36
 
34
37
  }
35
38
 
36
- declare global {
37
- namespace Config {
38
- interface EmailTransporters { }
39
- }
40
- }
41
-
42
39
  /*----------------------------------
43
40
  - TYPES: EMAILS
44
41
  ----------------------------------*/
45
42
 
43
+ export { Transporter } from './transporter';
44
+
46
45
  export type TEmail = THtmlEmail | TTemplateEmail;
47
46
 
48
47
  type TBaseEmail = {
@@ -68,11 +67,6 @@ export type TCompleteEmail = With<THtmlEmail, {
68
67
  /*----------------------------------
69
68
  - TYPES: OPTIONS
70
69
  ----------------------------------*/
71
-
72
- export abstract class Transporter {
73
- public abstract send( emails: TCompleteEmail[] ): Promise<void>;
74
- }
75
-
76
70
  type TOptions = {
77
71
  transporter?: string,
78
72
  testing?: boolean
@@ -82,6 +76,8 @@ type TOptions = {
82
76
  - FONCTIONS
83
77
  ----------------------------------*/
84
78
  export default class Email extends Service<Config, Hooks, Application> {
79
+
80
+ private transporters = this.config.transporters;
85
81
 
86
82
  public async register() {
87
83
 
@@ -91,11 +87,6 @@ export default class Email extends Service<Config, Hooks, Application> {
91
87
 
92
88
  }
93
89
 
94
- private transporters = {} as {[name: string]: Transporter};
95
- public addTransporter( name: string, transporter: (new () => Transporter) ) {
96
- console.log(`[email] registering email transporter: ${name}`);
97
- this.transporters[ name ] = new transporter();
98
- }
99
90
 
100
91
  public async send(
101
92
  emails: TEmail | TEmail[],
@@ -119,6 +110,7 @@ export default class Email extends Service<Config, Hooks, Application> {
119
110
  : email.to;
120
111
 
121
112
  // Via template
113
+ // TODO: Restore templates feature
122
114
  if ('template' in email) {
123
115
 
124
116
  const template = templates[email.template];
@@ -170,8 +162,8 @@ export default class Email extends Service<Config, Hooks, Application> {
170
162
  return;
171
163
  }
172
164
 
173
- const Transporter = this.transporters[ transporterName ];
174
- await Transporter.send(emailsToSend);
165
+ const transporter = this.transporters[ transporterName ];
166
+ await transporter.send(emailsToSend);
175
167
 
176
168
  }
177
169
  }
@@ -0,0 +1,38 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+
7
+ // Core
8
+ import type Application from "@server/app";
9
+ import type EmailService from '@server/services/email';
10
+
11
+ // Specific
12
+ import type { TCompleteEmail } from ".";
13
+
14
+ /*----------------------------------
15
+ - TYPES
16
+ ----------------------------------*/
17
+
18
+ export type TBasicConfig = {
19
+ api: string,
20
+ debug: boolean
21
+ }
22
+
23
+ /*----------------------------------
24
+ - CLASS
25
+ ----------------------------------*/
26
+ export abstract class Transporter<TConfig extends {} = {}> {
27
+
28
+ public constructor(
29
+ protected app: Application & { email: EmailService },
30
+ protected config: TBasicConfig & TConfig,
31
+
32
+ protected email = app.email
33
+ ) {
34
+
35
+ }
36
+
37
+ public abstract send( emails: TCompleteEmail[] ): Promise<void>;
38
+ }
@@ -24,7 +24,7 @@ import BaseRouter, {
24
24
  } from '@common/router';
25
25
  import { buildRegex, getRegisterPageArgs } from '@common/router/register';
26
26
  import { layoutsList } from '@common/router/layouts';
27
- import { TFetcherArgs } from '@common/router/request/api';
27
+ import { TFetcherList, TFetcher } from '@common/router/request/api';
28
28
  import type { TFrontRenderer } from '@common/router/response/page';
29
29
  import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@client/services/router';
30
30
 
@@ -386,7 +386,7 @@ export default class ServerRouter<
386
386
  // Bulk API Requests
387
387
  if (request.path === '/api' && typeof request.data.fetchers === "object") {
388
388
 
389
- return await this.resolveApiBatch(request);
389
+ return await this.resolveApiBatch(request.data.fetchers, request);
390
390
 
391
391
  } else {
392
392
  response = await this.resolve(request);
@@ -434,7 +434,7 @@ export default class ServerRouter<
434
434
 
435
435
  public async resolve(request: ServerRequest<this>): Promise<ServerResponse<this>> {
436
436
 
437
- console.info(request.ip, request.method, request.domain, request.path, 'user =', request.user);
437
+ console.info(request.ip, request.method, request.domain, request.path);
438
438
 
439
439
  const response = new ServerResponse<this>(request);
440
440
 
@@ -475,12 +475,14 @@ export default class ServerRouter<
475
475
  throw new NotFound(`The requested endpoint was not found.`);
476
476
  }
477
477
 
478
- private async resolveApiBatch( request: ServerRequest<this> ) {
478
+ private async resolveApiBatch( fetchers: TFetcherList, request: ServerRequest<this> ) {
479
+
480
+ // TODO: use api.fetchSync instead
479
481
 
480
482
  const responseData: TObjetDonnees = {};
481
- for (const id in request.data.fetchers) {
483
+ for (const id in fetchers) {
482
484
 
483
- const [method, path, data] = request.data.fetchers[id] as TFetcherArgs;
485
+ const { method, path, data } = fetchers[id];
484
486
 
485
487
  const response = await this.resolve(
486
488
  request.children(method, path, data)
@@ -489,7 +491,6 @@ export default class ServerRouter<
489
491
  responseData[id] = response.data;
490
492
 
491
493
  // TODO: merge response.headers ?
492
-
493
494
  }
494
495
 
495
496
  // Status
@@ -52,22 +52,25 @@ export default class ApiClientRequest extends RequestService implements ApiClien
52
52
  };
53
53
  }
54
54
 
55
- public async fetchSync(fetchers: TFetcherList): Promise<TObjetDonnees> {
55
+ public async fetchSync(fetchers: TFetcherList, alreadyLoadedData: {}): Promise<TObjetDonnees> {
56
56
 
57
- const resolved: TObjetDonnees = {};
57
+ const fetchedData: TObjetDonnees = { ...alreadyLoadedData };
58
58
 
59
59
  for (const id in fetchers) {
60
60
 
61
61
  const { method, path, data, options } = fetchers[id];
62
-
63
62
  //this.router.config.debug && console.log(`[api] Resolving from internal api`, method, path, data);
64
63
 
64
+ // We don't fetch the already given data
65
+ if (id in fetchedData)
66
+ continue;
67
+
68
+ // Create a children request to resolve the api data
65
69
  const internalHeaders = { accept: 'application/json' }
66
70
  const request = this.request.children(method, path, data, { ...internalHeaders/*, ...headers*/ });
67
- resolved[id] = await request.router.resolve(request).then(res => res.data);
68
-
71
+ fetchedData[id] = await request.router.resolve(request).then(res => res.data);
69
72
  }
70
73
 
71
- return resolved;
74
+ return fetchedData;
72
75
  }
73
76
  }
@@ -107,11 +107,16 @@ export default class ServerResponse<
107
107
  if (response === undefined)
108
108
  return;
109
109
 
110
- // Default data type for `return <raw data>`
111
- if (response instanceof Page)
110
+ // No need to process the response
111
+ if (response instanceof ServerResponse)
112
+ return;
113
+ // Render react page to html
114
+ else if (response instanceof Page)
112
115
  await this.render(response, context, additionnalData);
116
+ // Return HTML
113
117
  else if (typeof response === 'string' && this.route.options.accept === 'html')
114
118
  await this.html(response);
119
+ // Return JSON
115
120
  else
116
121
  await this.json(response);
117
122
  }
@@ -186,7 +191,7 @@ export default class ServerResponse<
186
191
  context.page = page;
187
192
 
188
193
  // Prepare page & fetch data
189
- await page.fetchData();
194
+ page.data = await page.fetchData();
190
195
  if (additionnalData !== undefined) // Example: error message for error pages
191
196
  page.data = { ...page.data, ...additionnalData }
192
197
 
@@ -162,7 +162,7 @@ export default abstract class UsersManagementService<
162
162
 
163
163
  const user = request.user;
164
164
 
165
- this.config.debug && console.warn(LogPrefix, `Check auth, role = ${role}. Current user =`, user);
165
+ this.config.debug && console.warn(LogPrefix, `Check auth, role = ${role}. Current user =`, this.displayName(user));
166
166
 
167
167
  if (user === undefined) {
168
168
 
File without changes
@@ -1,31 +0,0 @@
1
- .white-card() {
2
- background: white;
3
- box-shadow: 0 3px 2px fade(#000, 10%);
4
- border: solid 1px fade(#000, 10%);
5
- border-radius: @radius;
6
- }
7
-
8
- .card,
9
- .input.text,
10
- .btn,
11
- .table,
12
- i.solid {
13
-
14
- .build-theme-bg( #fff, #8E8E8E);
15
-
16
- &:not(.bg) {
17
- .white-card();
18
- }
19
-
20
- .bg & {
21
- box-shadow: none;
22
- border: none;
23
- }
24
- }
25
-
26
- //.card.clickable:hover,
27
- .btn:hover,
28
- .input.text.focus {
29
- z-index: 5;
30
- box-shadow: 0 10px 50px fade(#000, 15%);
31
- }