5htp-core 0.3.8 → 0.3.9

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 (30) hide show
  1. package/package.json +2 -2
  2. package/src/client/assets/css/components/button.less +6 -10
  3. package/src/client/assets/css/components/card.less +1 -7
  4. package/src/client/assets/css/text/icons.less +8 -5
  5. package/src/client/assets/css/text/text.less +2 -3
  6. package/src/client/assets/css/theme.less +1 -1
  7. package/src/client/assets/css/utils/layouts.less +4 -0
  8. package/src/client/components/Form.ts +2 -1
  9. package/src/client/components/Select/ChoiceElement.tsx +20 -6
  10. package/src/client/components/Select/ChoiceSelector.tsx +3 -2
  11. package/src/client/components/Select/index.tsx +53 -21
  12. package/src/client/components/containers/Popover/index.tsx +4 -1
  13. package/src/client/components/data/Time.tsx +16 -12
  14. package/src/client/components/inputv3/base.tsx +1 -0
  15. package/src/client/components/inputv3/index.tsx +3 -1
  16. package/src/client/services/router/components/router.tsx +10 -8
  17. package/src/client/services/router/index.tsx +0 -0
  18. package/src/client/services/router/request/api.ts +9 -3
  19. package/src/common/data/dates.ts +20 -4
  20. package/src/common/data/objets.ts +17 -0
  21. package/src/common/validation/schema.ts +85 -91
  22. package/src/common/validation/validator.ts +4 -6
  23. package/src/common/validation/validators.ts +75 -72
  24. package/src/server/app/service/index.ts +1 -1
  25. package/src/server/app.tsconfig.json +1 -1
  26. package/src/server/services/database/connection.ts +3 -1
  27. package/src/server/services/database/index.ts +22 -22
  28. package/src/server/services/database/model.ts +28 -0
  29. package/src/server/services/router/index.ts +4 -1
  30. package/src/server/services/schema/request.ts +4 -11
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
3
  "description": "Convenient TypeScript framework designed for Performance and Productivity.",
4
- "version": "0.3.8",
4
+ "version": "0.3.9",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -99,6 +99,6 @@
99
99
  "babel-plugin-glob-import": "^0.0.7"
100
100
  },
101
101
  "peerDependencies": {
102
- "5htp": "0.3.1"
102
+ "5htp": "0.3.8"
103
103
  }
104
104
  }
@@ -25,7 +25,7 @@
25
25
 
26
26
  // Text
27
27
  text-decoration: none;
28
- font-weight: 600;
28
+ font-weight: 500;
29
29
 
30
30
  // Colors
31
31
  background: var(--cBg);
@@ -90,12 +90,6 @@
90
90
 
91
91
  }
92
92
 
93
- > .pastille {
94
- position: absolute;
95
- right: 10px;
96
- bottom: 10px;
97
- }
98
-
99
93
  /*----------------------------------
100
94
  - THEME
101
95
  ----------------------------------*/
@@ -230,9 +224,12 @@ ul.row {
230
224
  color: var(--cTxtImportant);
231
225
  }
232
226
 
233
- // All the list items label must be aligned
234
227
  > .label {
228
+ // All the list items label must be aligned
235
229
  justify-content: flex-start;
230
+ // Since they're all horizontally aligned,
231
+ // Label = max width, so icon right are also aligned to right
232
+ flex: 1;
236
233
  }
237
234
 
238
235
  &.icon {
@@ -244,8 +241,7 @@ ul.row {
244
241
  display: none;
245
242
  position: absolute;
246
243
 
247
- background: fade(@cDark, 90%);
248
- backdrop-filter: blur(20px) saturate(180%);
244
+ background: #111;
249
245
 
250
246
  height: @sizeComponent;
251
247
  line-height: @sizeComponent;
@@ -77,7 +77,7 @@
77
77
  }
78
78
  }
79
79
 
80
- &.selected {
80
+ &.active {
81
81
 
82
82
  box-shadow: 0 0 0 3px @c1;
83
83
 
@@ -86,12 +86,6 @@
86
86
  }
87
87
  }
88
88
 
89
- &.minimal {
90
- background:transparent;
91
- box-shadow: none;
92
- border: solid 1px var(--cLine);
93
- }
94
-
95
89
  /*----------------------------------
96
90
  - VARIANTS
97
91
  ----------------------------------*/
@@ -33,11 +33,14 @@ i {
33
33
 
34
34
  &.solid {
35
35
  color: var(--cAccent2);
36
-
37
- width: @sizeComponent;
38
- flex: 0 0 @sizeComponent;
39
- height: @sizeComponent;
40
- line-height: @sizeComponent;
36
+ background: var(--cBg);
37
+ border-radius: @radius;
38
+
39
+ // Normla size must fit inside a normal fit element
40
+ width: @sizeComponent * 0.75;
41
+ flex: 0 0 @sizeComponent * 0.75;
42
+ height: @sizeComponent * 0.75;
43
+ line-height: @sizeComponent * 0.75;
41
44
  //font-size: 0.9em;
42
45
  }
43
46
 
@@ -103,8 +103,7 @@ strong {
103
103
  }
104
104
  }
105
105
 
106
- strong.number,
107
- header > strong {
106
+ strong.number {
108
107
  //font-family: 'Montserrat';
109
108
  font-size: 1.3em;
110
109
  line-height: 1em;
@@ -181,7 +180,7 @@ pre {
181
180
 
182
181
  h2, h3, h4 {
183
182
  text-align: left;
184
- margin: @spacing * 2 0;
183
+ margin: 1em 0;
185
184
  }
186
185
 
187
186
  h2 {
@@ -32,7 +32,7 @@
32
32
  // Flags
33
33
  @bg: @theme[background];
34
34
  @bgActive: if( @isLight,
35
- @bg - #111,
35
+ @bg - #040404,
36
36
  @bg + #111,
37
37
  );
38
38
  @fg: @theme[foreground];
@@ -37,6 +37,10 @@
37
37
  > * {
38
38
  min-width: fit-content;
39
39
  }
40
+
41
+ &.menu {
42
+ min-height: @sizeComponent;
43
+ }
40
44
  }
41
45
 
42
46
  // Avec justify-content: center, les premiers élements sont cachés
@@ -109,7 +109,7 @@ export default function useForm<TFormData extends {}>(
109
109
  ----------------------------------*/
110
110
  const validate = (allData: Partial<TFormData> = data, validateAll: boolean = true) => {
111
111
 
112
- const validated = schema.validate(allData, allData, {}, {
112
+ const validated = schema.validateWithDetails(allData, allData, {}, {
113
113
  // Ignore the fields where the vlaue has not been changed
114
114
  // if the validation was triggered via onChange
115
115
  ignoreMissing: !validateAll,
@@ -138,6 +138,7 @@ export default function useForm<TFormData extends {}>(
138
138
  context.app.handleError(
139
139
  new InputError("You have " + validated.errorsCount + " errors in the form.")
140
140
  );
141
+ console.log("validated", validated.erreurs);
141
142
  return;
142
143
  }
143
144
 
@@ -5,10 +5,11 @@
5
5
  // Npm
6
6
  import React from 'react';
7
7
 
8
+ // Cpre
9
+ import { Button } from '@client/components';
10
+
8
11
  // Specific
9
- import type {
10
- Choice,
11
- } from './ChoiceSelector';
12
+ import type { Choice } from './ChoiceSelector';
12
13
 
13
14
  import type { Props } from '.';
14
15
 
@@ -19,10 +20,11 @@ import type { Props } from '.';
19
20
  /*----------------------------------
20
21
  - COMPONENT
21
22
  ----------------------------------*/
22
- export default ({ choice, currentList, onChange, multiple, includeCurrent }: {
23
+ export default ({ choice, currentList, onChange, multiple, includeCurrent, format = 'badge' }: {
23
24
  choice: Choice,
24
25
  currentList: Choice[],
25
- includeCurrent: boolean
26
+ includeCurrent?: boolean,
27
+ format?: 'list' | 'badge'
26
28
  } & Pick<Props, 'onChange'|'multiple'>) => {
27
29
 
28
30
  const isCurrent = currentList.some(c => c.value === choice.value);
@@ -30,7 +32,19 @@ export default ({ choice, currentList, onChange, multiple, includeCurrent }: {
30
32
 
31
33
  const showRemoveButton = multiple;
32
34
 
33
- return isCurrent ? (
35
+ return format === 'list' ? (
36
+ <li>
37
+ <Button icon={isCurrent ? 'check-circle' : undefined} onClick={() => onChange( current => multiple
38
+ ? (isCurrent
39
+ ? currentList.filter(item => item.value !== choice.value)
40
+ : [...(current || []), choice]
41
+ )
42
+ : isCurrent ? undefined : choice
43
+ )}>
44
+ {choice.label}
45
+ </Button>
46
+ </li>
47
+ ) : isCurrent ? (
34
48
  <li class={"badge bg primary"+ (showRemoveButton ? ' pdr-05' : '')}>
35
49
  {choice.label}
36
50
 
@@ -25,14 +25,14 @@ type ChoicesFunc = (search: string) => Promise<Choices>
25
25
  export type Props = (
26
26
  {
27
27
  multiple: true,
28
- value?: Choice[],
28
+ value?: Choice[] | Choice["value"][],
29
29
  onChange: StateUpdater<Choice[]>,
30
30
  validator?: ArrayValidator
31
31
  }
32
32
  |
33
33
  {
34
34
  multiple?: false,
35
- value?: Choice,
35
+ value?: Choice | Choice["value"],
36
36
  onChange: StateUpdater<Choice>,
37
37
  validator?: StringValidator
38
38
  }
@@ -53,6 +53,7 @@ export type Props = (
53
53
  - we don't want the selector to be rendered before the dropdown content is dhown
54
54
  - this component is called multiple time
55
55
  */
56
+ // ! OBSOLETE
56
57
  export default React.forwardRef<HTMLDivElement, Props>(({
57
58
  choices: initChoices,
58
59
  validator,
@@ -29,6 +29,25 @@ export type Props = DropdownProps & SelectorProps & {
29
29
 
30
30
  export type { Choice } from './ChoiceSelector';
31
31
 
32
+ const ensureChoice = (choice: Choice | string, choices: Choice[]): Choice => {
33
+
34
+ // Allready a choice
35
+ if (typeof choice === 'object' && choice.label) {
36
+ return choice;
37
+ }
38
+
39
+ // Find the choice
40
+ const found = choices.find( c => c.value === choice);
41
+ if (found)
42
+ return found;
43
+
44
+ // Create a new choice
45
+ return {
46
+ label: choice,
47
+ value: choice
48
+ }
49
+ }
50
+
32
51
  /*----------------------------------
33
52
  - COMONENT
34
53
  ----------------------------------*/
@@ -95,8 +114,8 @@ export default ({
95
114
  const currentList: Choice[] = current === undefined
96
115
  ? []
97
116
  : (Array.isArray(current)
98
- ? current
99
- : [current]
117
+ ? current.map( c => ensureChoice(c, choices))
118
+ : [ensureChoice(current, choices)]
100
119
  );
101
120
 
102
121
  /*----------------------------------
@@ -121,14 +140,7 @@ export default ({
121
140
  - RENDER
122
141
  ----------------------------------*/
123
142
 
124
- const SelectedItems = ( enableSearch ? currentList : choices ).map( choice => (
125
- <ChoiceElement choice={choice}
126
- currentList={currentList}
127
- onChange={onChange}
128
- multiple={multiple}
129
- includeCurrent
130
- />
131
- ))
143
+ const selectedItems = enableSearch ? currentList : choices
132
144
 
133
145
  const Search = enableSearch && (
134
146
  <Input
@@ -145,7 +157,7 @@ export default ({
145
157
  overflowY: 'auto'
146
158
  }}>
147
159
  {choices.map( choice => (
148
- <ChoiceElement choice={choice}
160
+ <ChoiceElement format='badge' choice={choice}
149
161
  currentList={currentList}
150
162
  onChange={onChange}
151
163
  multiple={multiple}
@@ -156,14 +168,21 @@ export default ({
156
168
 
157
169
  return dropdown ? (
158
170
  <Popover content={(
159
- <div class="card col" style={{ width: '300px' }}>
171
+ <div class="card col" style={{ width: '200px' }}>
160
172
 
161
173
  <div class="col">
162
174
 
163
- {SelectedItems.length !== 0 && (
164
- <div class="row wrap">
165
- {SelectedItems}
166
- </div>
175
+ {selectedItems.length !== 0 && (
176
+ <ul class="menu col">
177
+ {selectedItems.map( choice => (
178
+ <ChoiceElement format='list' choice={choice}
179
+ currentList={currentList}
180
+ onChange={onChange}
181
+ multiple={multiple}
182
+ includeCurrent
183
+ />
184
+ ))}
185
+ </ul>
167
186
  )}
168
187
 
169
188
  {Search}
@@ -173,10 +192,15 @@ export default ({
173
192
  </div>
174
193
  )} state={popoverState}>
175
194
  <Button icon={icon} iconR="chevron-down" {...otherProps}>
176
- {title} {(multiple && currentList.length > 0)
177
- ? <span class="badge s bg accent">{currentList.length}</span>
178
- : null
179
- }
195
+
196
+ {currentList.length === 0 ? <>
197
+ {title}
198
+ </> : multiple ? <>
199
+ {title} <span class="badge s bg accent">{currentList.length}</span>
200
+ </> : <>
201
+ {currentList[0].label}
202
+ </>}
203
+
180
204
  </Button>
181
205
  </Popover>
182
206
  ) : (
@@ -197,7 +221,15 @@ export default ({
197
221
  )}</label>
198
222
 
199
223
  <div class="row al-left wrap sp-05">
200
- {SelectedItems}
224
+
225
+ {selectedItems.map( choice => (
226
+ <ChoiceElement format='badge' choice={choice}
227
+ currentList={currentList}
228
+ onChange={onChange}
229
+ multiple={multiple}
230
+ includeCurrent
231
+ />
232
+ ))}
201
233
 
202
234
  {Search}
203
235
  </div>
@@ -21,7 +21,7 @@ export type Props = JSX.HTMLAttributes<HTMLDivElement> & {
21
21
 
22
22
  // Display
23
23
  content?: JSX.Element,
24
- state: [boolean, StateUpdater<boolean>],
24
+ state?: [boolean, StateUpdater<boolean>],
25
25
  width?: number | string,
26
26
  disable?: boolean
27
27
  // Position
@@ -58,6 +58,9 @@ export default (props: Props) => {
58
58
  const refCont = React.useRef<HTMLElement>(null);
59
59
  const refContent = React.useRef<HTMLElement>(null);
60
60
 
61
+ if (state === undefined)
62
+ state = React.useState(false);
63
+
61
64
  const [shown, show] = state;
62
65
 
63
66
  // Màj visibilite
@@ -4,36 +4,40 @@
4
4
 
5
5
  // Npm
6
6
  import React from 'react';
7
+ import { ComponentChild } from 'preact';
7
8
 
8
9
  // Libs
9
- import { timeSince } from '@common/data/dates';
10
+ import { timeSince, TDateInfo } from '@common/data/dates';
10
11
 
11
12
  /*----------------------------------
12
- - TYPES: IMPORTATIONS
13
+ - TYPES
13
14
  ----------------------------------*/
15
+ export type { TDateInfo } from '@common/data/dates';
14
16
 
15
-
16
-
17
- /*----------------------------------
18
- - TYPES: DECLARATIONS
19
- ----------------------------------*/
17
+ export const interval = {
18
+ day: 24 * 60 * 60,
19
+ hour: 60 * 60,
20
+ minute: 60,
21
+ second: 1
22
+ }
20
23
 
21
24
  /*----------------------------------
22
25
  - COMPOSANT
23
26
  ----------------------------------*/
24
- export default ({ since }: {
25
- since: Parameters<typeof timeSince>[0]
27
+ export default ({ since, render }: {
28
+ since: Parameters<typeof timeSince>[0],
29
+ render?: (dateInfo: TDateInfo) => ComponentChild
26
30
  }) => {
27
31
 
28
- const [text, setDisplay] = React.useState(timeSince(since));
32
+ const [time, setTime] = React.useState<TDateInfo | null>( timeSince(since) );
29
33
 
30
34
  React.useEffect(() => {
31
35
 
32
- const textUpdate = setInterval(() => setDisplay(timeSince(since)), 10000);
36
+ const textUpdate = setInterval(() => setTime(timeSince(since)), 10000);
33
37
  return () => clearInterval(textUpdate);
34
38
 
35
39
  }, []);
36
40
 
37
- return <>{text}</>;
41
+ return <>{time === null ? "?" : render ? render(time) : time.text}</>;
38
42
 
39
43
  }
@@ -18,6 +18,7 @@ export type InputBaseProps<TValue> = {
18
18
  title: string, // Now mandatory
19
19
  required?: boolean,
20
20
  errors?: string[],
21
+ size?: TComponentSize,
21
22
 
22
23
  value: TValue,
23
24
  onChange?: (value: TValue) => void,
@@ -44,7 +44,7 @@ export type Props = {
44
44
  ----------------------------------*/
45
45
  export default ({
46
46
  // Decoration
47
- icon, prefix, suffix, iconR, required,
47
+ icon, prefix, suffix, iconR, required, size,
48
48
  // State
49
49
  inputRef, errors,
50
50
  // Behavior
@@ -142,6 +142,8 @@ export default ({
142
142
  className += ' empty';
143
143
  if (focus)
144
144
  className += ' focus';
145
+ if (size !== undefined)
146
+ className += ' ' + size;
145
147
  if (errors?.length)
146
148
  className += ' error';
147
149
 
@@ -85,13 +85,8 @@ export default ({ service: clientRouter }: { service?: ClientRouter }) => {
85
85
  // WARNING: Don"t try to play with pages here, since the object will not be updated
86
86
  // If needed to play with pages, do it in the setPages callback below
87
87
  // Unchanged path
88
- if (request.path === currentRequest.path) {
89
-
90
- // Scroll to component
91
- if (request.hash) {
92
- scrollToElement(request.hash);
93
- }
94
-
88
+ if (request.path === currentRequest.path && request.hash !== currentRequest.hash) {
89
+ scrollToElement(request.hash);
95
90
  return;
96
91
  }
97
92
 
@@ -110,7 +105,14 @@ export default ({ service: clientRouter }: { service?: ClientRouter }) => {
110
105
  }
111
106
 
112
107
  // Fetch API data to hydrate the page
113
- const newData = await newpage.fetchData();
108
+ let newData;
109
+ try {
110
+ newData = await newpage.fetchData();
111
+ } catch (error) {
112
+ console.error(LogPrefix, "Unable to fetch data:", error);
113
+ clientRouter.setLoading(false);
114
+ return;
115
+ }
114
116
 
115
117
  // Add page container
116
118
  setPages( pages => {
File without changes
@@ -163,7 +163,7 @@ export default class ApiClient implements ApiClientService {
163
163
 
164
164
  return data;
165
165
 
166
- });
166
+ })
167
167
 
168
168
  // Errors will be catched in the caller
169
169
 
@@ -229,14 +229,20 @@ export default class ApiClient implements ApiClientService {
229
229
 
230
230
  })
231
231
  .catch((e: AxiosError) => {
232
-
232
+
233
233
  if (e.response !== undefined) {
234
234
 
235
+ // Transmiss error
235
236
  console.warn(`[api] Failure:`, e);
236
- throw viaHttpCode(
237
+ const error = viaHttpCode(
237
238
  e.response.status || 500,
238
239
  e.response.data
239
240
  );
241
+
242
+ // API Error hook
243
+ this.app.handleError(error, e.response.status);
244
+
245
+ throw error;
240
246
 
241
247
  // Erreur réseau: l'utilisateur n'ets probablement plus connecté à internet
242
248
  } else {
@@ -5,19 +5,35 @@ const timeAgo = new TimeAgo('en-US')
5
5
 
6
6
  import dayjs from 'dayjs';
7
7
 
8
- export const timeSince = (date: Date | number | string) => {
8
+ export type TDateInfo = {
9
+ isPast: boolean,
10
+ delta: number,
11
+ text: string
12
+ }
13
+
14
+ export const timeSince = (date: Date | number | string): TDateInfo | null => {
9
15
 
10
16
  if (date === undefined)
11
- return 'Inconnu';
17
+ return null;
12
18
 
13
19
  // Timeago ne prend que des dates et des timestamp
14
20
  if (typeof date === 'string') {
15
21
  date = Date.parse(date);
16
22
  if (isNaN(date))
17
- return "?";
23
+ return null;
18
24
  }
19
25
 
20
- return timeAgo.format(date);
26
+ // Get metas
27
+ const now = Date.now()
28
+ const timestamp = date instanceof Date ? date.getTime() : date;
29
+ const deltaSeconds = Math.abs( Math.round( (now - timestamp) / 1000 ));
30
+ const isPast = now > timestamp;
31
+
32
+ return {
33
+ text: timeAgo.format(date),
34
+ isPast,
35
+ delta: deltaSeconds
36
+ };
21
37
  }
22
38
 
23
39
  export const tempsRelatif = (time: number, nbChiffresInit?: number) => {
@@ -117,4 +117,21 @@ export const chemin = {
117
117
  valA[ brancheVal ] = val;
118
118
 
119
119
  }
120
+ }
121
+
122
+ export const groupBy = <TObj extends TObjetDonnees>(
123
+ items: TObj[],
124
+ key: keyof TObj
125
+ ): {[key: string]: TObj[]} => {
126
+
127
+ const grouped: {[key: string]: TObj[]} = {};
128
+
129
+ for (const item of items) {
130
+ const indexValue = item[key] as any;
131
+ if (grouped[ indexValue ] === undefined)
132
+ grouped[ indexValue ] = [];
133
+ grouped[ indexValue ].push(item);
134
+ }
135
+
136
+ return grouped;
120
137
  }
@@ -12,7 +12,7 @@ import { default as Validator, EXCLUDE_VALUE } from './validator';
12
12
  - TYPES
13
13
  ----------------------------------*/
14
14
 
15
- export type TSchemaFields = { [fieldName: string]: Schema<{}> | Validator<any> }
15
+ export type TSchemaFields = { [fieldName: string]: TSchemaFields | Schema<{}> | Validator<any> }
16
16
 
17
17
  type TSchemaOptions = {
18
18
  opt?: boolean
@@ -60,134 +60,128 @@ export default class Schema<TFields extends TSchemaFields> {
60
60
  }
61
61
 
62
62
  public validate<TDonnees extends TObjetDonnees>(
63
-
64
63
  dataToValidate: Partial<TDonnees>,
65
- allData: TDonnees,
66
- output: TObjetDonnees = {},
67
-
68
64
  opts: TValidateOptions<TFields> = {},
69
65
  chemin: string[] = []
66
+ ): TValidatedData<TFields> {
67
+
68
+ // Check data type
69
+ if (typeof dataToValidate !== 'object')
70
+ throw new InputErrorSchema({ [chemin.join('.')]: ['Must be an object'] });
70
71
 
71
- ): TValidationResult<TFields> {
72
-
72
+ // Default options
73
73
  opts = {
74
74
  debug: false,
75
- throwError: false,
75
+ throwError: true,
76
76
  validateDeps: true,
77
77
  autoCorrect: false,
78
78
  ...opts,
79
79
  }
80
-
81
- let outputSchema = output;
82
- for (const branche of chemin)
83
- outputSchema = outputSchema[branche];
84
80
 
85
- const keysToValidate = opts.only || Object.keys(this.fields);
81
+ const keysToValidate = (opts.only || Object.keys(this.fields)) as string[];
86
82
 
87
83
  // Validation de chacune d'entre elles
84
+ const output: Partial<TDonnees> = {};
88
85
  let erreurs: TListeErreursSaisie = {};
89
86
  let errorsCount = 0;
90
87
  for (const fieldName of keysToValidate) {
91
88
 
92
89
  // La donnée est répertoriée dans le schema
93
- const field = this.fields[fieldName];
90
+ let field = this.fields[fieldName];
91
+ let validator: Validator<any> | Schema<{}>;
94
92
  if (field === undefined) {
95
93
  opts.debug && console.warn(LogPrefix, '[' + fieldName + ']', 'Exclusion (pas présent dans le schéma)');
96
94
  continue;
97
- }
95
+ } else if (field.constructor === Object)
96
+ validator = new Schema(field as TSchemaFields);
97
+ else
98
+ validator = field as Validator<any>;
98
99
 
100
+ // Create field path
99
101
  const cheminA = [...chemin, fieldName]
100
102
  const cheminAstr = cheminA.join('.')
101
-
102
- // Sous-schema
103
- if (field instanceof Schema) {
104
-
105
- // Initialise la structure pour permettre l'assignement d'outputSchema
106
- if (outputSchema[fieldName] === undefined)
107
- outputSchema[fieldName] = {}
108
-
109
- // The corresponding data should be an object
110
- const schemadata = dataToValidate[fieldName];
111
- if (typeof schemadata !== 'object') {
112
- erreurs[ cheminAstr ] = [`Should be an object`];
103
+ const valOrigine = dataToValidate[fieldName];
104
+
105
+ // Validation
106
+ try {
107
+
108
+ const val = validator.validate(valOrigine, opts, cheminA);
109
+
110
+ // Exclusion seulement si explicitement demandé
111
+ // IMPORTANT: Conserver les values undefined
112
+ // La présence d'un valeur undefined peut être utile, par exemple, pour indiquer qu'on souhaite supprimer une donnée
113
+ // Exemple: undefinec = suppression fichier | Absende donnée = conservation fihcier actuel
114
+ if (val === EXCLUDE_VALUE)
115
+ opts.debug && console.log(LogPrefix, '[' + cheminA + '] Exclusion demandée');
116
+ else
117
+ output[fieldName] = val;
118
+
119
+ opts.debug && console.log(LogPrefix, '[' + cheminA + ']', valOrigine, '=>', val);
120
+
121
+ } catch (error) {
122
+
123
+ opts.debug && console.warn(LogPrefix, '[' + cheminA + ']', valOrigine, '|| CoreError:', error);
124
+
125
+ if (error instanceof InputErrorSchema) {
126
+
127
+ erreurs = { ...erreurs, ...error.erreursSaisie };
128
+ errorsCount += Object.keys(error.erreursSaisie).length;
129
+
130
+ } else if (error instanceof CoreError) {
131
+
132
+ erreurs[cheminAstr] = [error.message]
133
+ errorsCount++;
134
+
135
+ } else if (SERVER) {
136
+
137
+ // Server: transmiss error & report bug
138
+ throw error;
139
+
140
+ } else {
141
+
142
+ erreurs[cheminAstr] = ["Technical error while validating data"];
113
143
  errorsCount++;
114
- continue;
115
144
  }
145
+ }
146
+ }
116
147
 
117
- // Validate the data
118
- const validationSchema = field.validate(
119
-
120
- schemadata,
121
- allData,
122
- output,
123
-
124
- opts,
125
- cheminA
126
- );
127
- erreurs = { ...erreurs, ...validationSchema.erreurs };
128
- errorsCount += validationSchema.errorsCount;
129
-
130
- // Pas besoin d'assigner, car output est passé en référence
131
- //output[fieldName] = validationSchema.values;
148
+ if (errorsCount !== 0)
149
+ throw new InputErrorSchema(erreurs);
150
+
151
+ opts.debug && console.log(LogPrefix, '', dataToValidate, '=>', output);
132
152
 
153
+ return output as TValidatedData<TFields>;
154
+ }
155
+
156
+ public validateWithDetails<TDonnees extends TObjetDonnees>(
133
157
 
134
- // I don't remind what is options.activer about
135
- /*} else if (field.activer !== undefined && field.activer(allData) === false) {
158
+ dataToValidate: Partial<TDonnees>,
159
+ allData: TDonnees,
160
+ output: TObjetDonnees = {},
136
161
 
137
- delete outputSchema[fieldName];*/
162
+ opts: TValidateOptions<TFields> = {},
163
+ chemin: string[] = []
138
164
 
139
- // Validator
165
+ ): TValidationResult<TFields> {
166
+
167
+ let erreurs: TListeErreursSaisie = {};
168
+ let errorsCount = 0;
169
+
170
+ try {
171
+ this.validate(dataToValidate, opts, chemin);
172
+ } catch (error) {
173
+ if (error instanceof InputErrorSchema) {
174
+ erreurs = error.erreursSaisie;
175
+ errorsCount = Object.keys(erreurs).length;
140
176
  } else {
141
-
142
- // Champ composé de plusieurs values
143
- const valOrigine = field.options.as === undefined
144
- ? dataToValidate[fieldName]
145
- // Le fieldName regroupe plusieurs values (ex: Periode)
146
- : field.options.as.map((nomVal: string) => dataToValidate[nomVal])
147
-
148
- // Validation
149
- try {
150
-
151
- const val = field.validate(valOrigine, allData, output, opts);
152
-
153
- // Exclusion seulement si explicitement demandé
154
- // IMPORTANT: Conserver les values undefined
155
- // La présence d'un valeur undefined peut être utile, par exemple, pour indiquer qu'on souhaite supprimer une donnée
156
- // Exemple: undefinec = suppression fichier | Absende donnée = conservation fihcier actuel
157
- if (val === EXCLUDE_VALUE)
158
- opts.debug && console.log(LogPrefix, '[' + cheminA + '] Exclusion demandée');
159
- else
160
- outputSchema[fieldName] = val;
161
-
162
- opts.debug && console.log(LogPrefix, '[' + cheminA + ']', valOrigine, '=>', val);
163
-
164
- } catch (error) {
165
-
166
- opts.debug && console.warn(LogPrefix, '[' + cheminA + ']', valOrigine, '|| CoreError:', error);
167
-
168
- if (error instanceof CoreError) {
169
-
170
- // Référencement erreur
171
- erreurs[cheminAstr] = [error.message]
172
- errorsCount++;
173
-
174
- } else
175
- throw error;
176
- }
177
+ throw error;
177
178
  }
178
179
  }
179
-
180
- if (errorsCount !== 0 && opts.throwError === true) {
181
- throw new InputErrorSchema(erreurs);
182
- }
183
-
184
- opts.debug && console.log(LogPrefix, '', dataToValidate, '=>', output);
185
-
180
+
186
181
  return {
187
182
  values: output as TValidatedData<TFields>,
188
183
  erreurs,
189
184
  errorsCount,
190
185
  };
191
-
192
186
  }
193
187
  }
@@ -28,7 +28,6 @@ export type TValidator<TValue> = {
28
28
  dependances?: string[],
29
29
  opt?: true,
30
30
  defaut?: TValue,
31
- as?: string[], // Mapping personnalisé
32
31
 
33
32
  }
34
33
 
@@ -40,9 +39,8 @@ type TValidationArgs<TValue, TAllValues extends {}> = [
40
39
  // For the value given as input in the validation function,
41
40
  // Only the empty values were escluded
42
41
  val: TNonEmptyValue,
43
- input: TAllValues,
44
- output: Partial<TAllValues>,
45
- validateOptions?: TValidateOptions
42
+ validateOptions: TValidateOptions,
43
+ path: string[]
46
44
  ]
47
45
 
48
46
  type TValidationFunction<TValue, TAllValues extends {} = {}> = (
@@ -87,7 +85,7 @@ export default class Validator<
87
85
  public isEmpty = (val: any) => val === undefined || val === '' || val === null
88
86
 
89
87
  public validate(...[
90
- val, input, output, validateOptions
88
+ val, validateOptions, path
91
89
  ]: TValidationArgs<TValue, {}>): TValidateReturnType<TOptions, TValue> {
92
90
 
93
91
  // Required value
@@ -105,7 +103,7 @@ export default class Validator<
105
103
  }
106
104
 
107
105
  // Validate type
108
- return this.validateType(val, input, output, validateOptions) as TValidateReturnType<TOptions, TValue>;
106
+ return this.validateType(val, validateOptions, path) as TValidateReturnType<TOptions, TValue>;
109
107
  }
110
108
 
111
109
  }
@@ -17,7 +17,7 @@ import { InputError } from '@common/errors';
17
17
  import FileToUpload from '@client/components/inputv3/file/FileToUpload';
18
18
 
19
19
  // Speciific
20
- import Schema from './schema'
20
+ import Schema, { TSchemaFields } from './schema'
21
21
  import Validator, { TValidator } from './validator'
22
22
 
23
23
  // Components
@@ -32,6 +32,10 @@ export type TFileValidator = TValidator<FileToUpload> & {
32
32
  taille?: number
33
33
  }
34
34
 
35
+ type TSchemaSubtype = Schema<{}> | TSchemaFields;
36
+
37
+ type TSubtype = TSchemaSubtype | Validator<any>;
38
+
35
39
  /*----------------------------------
36
40
  - CONST
37
41
  ----------------------------------*/
@@ -48,30 +52,35 @@ export default class SchemaValidators {
48
52
  /*----------------------------------
49
53
  - CONTENEURS
50
54
  ----------------------------------*/
51
- public object = ({ ...opts }: TValidator<object> & {} = {}) =>
52
- new Validator<object>('object', (val, input, output) => {
55
+ public object = ( subtype?: TSchemaSubtype, { ...opts }: TValidator<object> & {
53
56
 
54
- // TODO: executer seulement coté serveur
55
- /*if (typeof val === 'string' && val.startsWith('{'))
56
- try {
57
- val = JSON.parse(val);
58
- } catch (error) {
59
- console.error('Unable to convert the given string into an object.');
60
- }*/
57
+ } = {}) =>
58
+ new Validator<object>('object', (val, options, path) => {
61
59
 
60
+ // The value should be an object
62
61
  if (typeof val !== 'object' || val.constructor !== Object)
63
62
  throw new InputError("This value must be an object.");
64
63
 
65
- return val;
64
+ // If no subtype, return the object as is
65
+ if (subtype === undefined)
66
+ return val;
67
+
68
+ // If subtype is a schema
69
+ const schema = subtype.constructor === Object
70
+ ? new Schema(subtype as TSchemaFields)
71
+ : subtype as Schema<{}>;
72
+
73
+ // Validate schema
74
+ const value = schema.validate(val, options, path);
75
+
76
+ return value;
66
77
  }, opts)
67
78
 
68
- public array = ( subtype?: Validator<any> | Schema<{}>, {
69
- choice, min, max, ...opts
70
- }: TValidator<any[]> & {
79
+ public array = ( subtype: TSubtype, { choice, min, max, ...opts }: TValidator<any[]> & {
71
80
  choice?: any[],
72
81
  min?: number,
73
82
  max?: number
74
- } = {}) => new Validator<any[]>('array', (items, input, output, corriger) => {
83
+ } = {}) => new Validator<any[]>('array', (items, options, path) => {
75
84
 
76
85
  // Type
77
86
  if (!Array.isArray(items))
@@ -84,32 +93,26 @@ export default class SchemaValidators {
84
93
  throw new InputError(`Please select maximum ${max} items.`);
85
94
 
86
95
  // Verif each item
87
- if (subtype !== undefined) {
88
- if (subtype instanceof Schema) {
96
+ if (subtype === undefined)
97
+ return items;
89
98
 
90
- items = items.map( item =>
91
- subtype.validate( item, item, item, { }, []).values
92
- )
99
+ const validator = subtype.constructor === Object
100
+ ? new Schema(subtype as TSchemaFields)
101
+ : subtype as Schema<{}> | Validator<any>;
93
102
 
94
- } else {
95
-
96
- items = items.map( item =>
97
- subtype.validate( item, items, items, corriger )
98
- )
99
-
100
- }
101
- }
103
+ items = items.map( item =>
104
+ validator.validate( item, options, path )
105
+ )
102
106
 
103
107
  return items;
104
108
  }, {
105
109
  ...opts,
106
110
  //multiple: true, // Sélection multiple
107
- //subtype
108
111
  })
109
112
 
110
113
  public choice = (choices?: any[], { multiple, ...opts }: TValidator<any> & {
111
114
  multiple?: boolean
112
- } = {}) => new Validator<any>('choice', (val, input, output) => {
115
+ } = {}) => new Validator<any>('choice', (val, options, path) => {
113
116
 
114
117
  // Empty array = undefined if not required
115
118
  if (val.length === 0 && opts.opt)
@@ -145,10 +148,11 @@ export default class SchemaValidators {
145
148
  /*----------------------------------
146
149
  - CHAINES
147
150
  ----------------------------------*/
148
- public string = ({ min, max, ...opts }: TValidator<string> & {
151
+ public string = ({ min, max, in: choices, ...opts }: TValidator<string> & {
149
152
  min?: number,
150
- max?: number
151
- } = {}) => new Validator<string>('string', (val, input, output, corriger?: boolean) => {
153
+ max?: number,
154
+ in?: string[]
155
+ } = {}) => new Validator<string>('string', (val, options, path) => {
152
156
 
153
157
  // Check type
154
158
  if (val === '')
@@ -161,13 +165,17 @@ export default class SchemaValidators {
161
165
  // Whitespace
162
166
  val = trim(val);
163
167
 
168
+ // In
169
+ if (choices !== undefined && !choices.includes(val))
170
+ throw new InputError(`Invalid value: ${val}. Must be one of: ${choices.join(', ')}`);
171
+
164
172
  // Min size
165
173
  if (min !== undefined && val.length < min)
166
174
  throw new InputError(`Must be at least ` + min + ' characters');
167
175
 
168
176
  // Max size
169
177
  if (max !== undefined && val.length > max)
170
- if (corriger)
178
+ if (options?.autoCorrect)
171
179
  val = val.substring(0, max);
172
180
  else
173
181
  throw new InputError(`Must be up to ` + max + ' characters');
@@ -179,9 +187,9 @@ export default class SchemaValidators {
179
187
  public url = (opts: TValidator<string> & {
180
188
  normalize?: NormalizeUrlOptions
181
189
  } = {}) =>
182
- new Validator<string>('url', (inputVal, input, output, corriger?) => {
190
+ new Validator<string>('url', (inputVal, options, path) => {
183
191
 
184
- let val = this.string(opts).validate(inputVal, input, output, corriger);
192
+ let val = this.string(opts).validate(inputVal, options, path);
185
193
 
186
194
  // Check if URL
187
195
  if (!isURL(val, {
@@ -197,9 +205,9 @@ export default class SchemaValidators {
197
205
  }, opts)
198
206
 
199
207
  public email = (opts: TValidator<string> & {} = {}) =>
200
- new Validator<string>('email', (inputVal, input, output, corriger?: boolean) => {
208
+ new Validator<string>('email', (inputVal, options, path) => {
201
209
 
202
- let val = this.string(opts).validate(inputVal, input, output, corriger);
210
+ let val = this.string(opts).validate(inputVal, options, path);
203
211
 
204
212
  if (!isEmail(val))
205
213
  throw new InputError("Please enter a valid email address.");
@@ -238,41 +246,36 @@ export default class SchemaValidators {
238
246
  min?: number,
239
247
  max?: number,
240
248
  step?: number,
241
- } = {}) => new Validator<number>('number', (val, input, output, corriger?: boolean) => {
242
-
243
- // Vérifications suivantes inutiles si des values spécifiques ont été fournies
244
- if (opts.in === undefined) {
249
+ } = {}) => new Validator<number>('number', (val, options, path) => {
245
250
 
246
- // Tente conversion chaine en nombre
247
- if (typeof val === 'string')
248
- val = withDecimals ? parseFloat(val) : parseInt(val);
249
-
250
- if (opts.min === undefined)
251
- opts.min = 0;
252
-
253
- // Type de donnée
254
- if (Number.isNaN(val) || typeof val !== 'number') {
255
- if (corriger)
256
- val = opts.min;
257
- else
258
- throw new InputError("This value must be a number.");
259
- }
260
-
261
- // Minimum
262
- if (val < opts.min)
263
- if (corriger)
264
- val = opts.min;
265
- else
266
- throw new InputError(`Must be at least ` + opts.min);
251
+ // Tente conversion chaine en nombre
252
+ if (typeof val === 'string')
253
+ val = withDecimals ? parseFloat(val) : parseInt(val);
254
+
255
+ if (opts.min === undefined)
256
+ opts.min = 0;
257
+
258
+ // Type de donnée
259
+ if (Number.isNaN(val) || typeof val !== 'number') {
260
+ if (options?.autoCorrect)
261
+ val = opts.min;
262
+ else
263
+ throw new InputError("This value must be a number.");
264
+ }
267
265
 
268
- // Maximum
269
- if (opts.max !== undefined && val > opts.max)
270
- if (corriger)
271
- val = opts.max;
272
- else
273
- throw new InputError(`Must be up to ` + opts.max);
266
+ // Minimum
267
+ if (val < opts.min)
268
+ if (options?.autoCorrect)
269
+ val = opts.min;
270
+ else
271
+ throw new InputError(`Must be at least ` + opts.min);
274
272
 
275
- }
273
+ // Maximum
274
+ if (opts.max !== undefined && val > opts.max)
275
+ if (options?.autoCorrect)
276
+ val = opts.max;
277
+ else
278
+ throw new InputError(`Must be up to ` + opts.max);
276
279
 
277
280
  return val;
278
281
  }, {
@@ -287,7 +290,7 @@ export default class SchemaValidators {
287
290
  public float = this.number(true)
288
291
 
289
292
  public bool = (opts: TValidator<boolean> & {} = {}) =>
290
- new Validator<boolean>('bool', (val, input, output) => {
293
+ new Validator<boolean>('bool', (val, options, path) => {
291
294
 
292
295
  if (typeof val !== 'boolean' && !['true', 'false'].includes(val))
293
296
  throw new InputError("This value must be a boolean.");
@@ -305,7 +308,7 @@ export default class SchemaValidators {
305
308
  ----------------------------------*/
306
309
  public date = (opts: TValidator<Date> & {
307
310
 
308
- } = {}) => new Validator<Date>('date', (val, input, output) => {
311
+ } = {}) => new Validator<Date>('date', (val, options, path) => {
309
312
 
310
313
  const chaine = typeof val == 'string';
311
314
 
@@ -58,7 +58,7 @@ export default abstract class Service<
58
58
  TConfig extends TServiceConfig,
59
59
  THooks extends THooksList,
60
60
  TApplication extends Application,
61
- TServicesIndex extends StartedServicesIndex
61
+ TServicesIndex extends StartedServicesIndex = {}
62
62
  > {
63
63
 
64
64
  public started?: Promise<void>;
@@ -5,7 +5,7 @@
5
5
  "baseUrl": "..",
6
6
  "paths": {
7
7
 
8
- "@/server/models": ["./server/.generated/models.d.ts"],
8
+ "@/server/models": ["./server/.generated/models.ts"],
9
9
 
10
10
  "@client/*": ["../node_modules/5htp-core/src/client/*"],
11
11
  "@common/*": ["../node_modules/5htp-core/src/common/*"],
@@ -188,7 +188,9 @@ export default class DatabaseManager {
188
188
 
189
189
  const table = db[field.table];
190
190
  if (table === undefined) {
191
- console.error("Field infos:", field);
191
+ // We don't throw error, sinc eit can be a virtual table
192
+ //console.error("Field infos:", field);
193
+ return next();
192
194
  throw new Error(`Table metadatas for ${field.db}.${field.table} were not loaded.`);
193
195
  }
194
196
 
@@ -385,7 +385,7 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
385
385
 
386
386
  // Build query
387
387
  return this.database.query(`
388
- UPDATE ${tableName} SET ${egalitesData} WHERE ${egalitesWhere};
388
+ UPDATE \`${tableName}\` SET ${egalitesData} WHERE ${egalitesWhere};
389
389
  `, opts);
390
390
 
391
391
  }
@@ -406,23 +406,37 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
406
406
  - OPERATIONS: INSERT
407
407
  ----------------------------------*/
408
408
 
409
- public tryInsert = (table: string, data: TObjetDonnees) =>
409
+ public tryInsert = <TData extends TObjetDonnees>(table: string, data: TData | TData[]) =>
410
410
  this.insert(table, data, { try: true });
411
411
 
412
+ public async insert<TData extends TObjetDonnees>(
413
+ path: string,
414
+ data: TData,
415
+ opts?: TInsertQueryOptions<TData>
416
+ ): Promise<TData>;
417
+
418
+ public async insert<TData extends TObjetDonnees>(
419
+ path: string,
420
+ data: TData[],
421
+ opts?: TInsertQueryOptions<TData[]>
422
+ ): Promise<TData>;
423
+
412
424
  public async insert<TData extends TObjetDonnees>(
413
425
  path: string,
414
426
  data: TData | TData[],
415
427
  opts: TInsertQueryOptions<TData> = {}
416
- ): Promise<OkPacket> {
428
+ ): Promise<TData | TData[]> {
417
429
 
418
430
  const table = this.database.getTable(path);
419
431
 
420
432
  // Normalize data
421
- if (!Array.isArray(data))
433
+ let returnSingleResult = false;
434
+ if (!Array.isArray(data)) {
422
435
  data = [data];
423
- else if (data.length === 0) {
436
+ returnSingleResult = true;
437
+ } else if (data.length === 0) {
424
438
  console.warn(LogPrefix, `Insert nothing in ${path}. Cancelled.`);
425
- return emptyOkPacket;
439
+ return [];
426
440
  }
427
441
 
428
442
  // Upsert
@@ -449,28 +463,14 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
449
463
  okPacket.warningCount += queryResult.warningCount;
450
464
  }
451
465
 
452
- return okPacket;
453
-
454
466
  } else {
455
467
  const query = this.buildInsertStatement(table, data, opts) + upsertStatement;
456
468
 
457
469
  const queryResult = await this.database.query<mysql.OkPacket>(query + ';', opts);
458
470
 
459
- return {
460
- constructor: {
461
- name: 'OkPacket'
462
- },
463
- fieldCount: queryResult.fieldCount,
464
- affectedRows: queryResult.affectedRows,
465
- changedRows: queryResult.changedRows,
466
- insertId: queryResult.insertId,
467
- serverStatus: queryResult.serverStatus,
468
- warningCount: queryResult.warningCount,
469
- message: queryResult.message,
470
- procotol41: queryResult.procotol41,
471
- };
472
471
  }
473
- // OLD: return [data, queryResult?.insertId, queryResult];
472
+
473
+ return returnSingleResult ? data[0] : data;
474
474
  }
475
475
 
476
476
  private buildInsertStatement<TData extends TObjetDonnees>(
@@ -0,0 +1,28 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ import type Application from "@server/app";
6
+
7
+ import type SQL from "@server/services/database";
8
+
9
+ /*----------------------------------
10
+ - TYPES
11
+ ----------------------------------*/
12
+
13
+ /*----------------------------------
14
+ - CLASS
15
+ ----------------------------------*/
16
+ export default abstract class Model<TData extends {}> {
17
+
18
+ public abstract tableName: string;
19
+
20
+ public constructor( app: Application & { SQL: SQL }, private SQL = app.SQL) {
21
+
22
+ }
23
+
24
+ public async create( data: TData | TData[] ): Promise< TData | TData[] > {
25
+ return await this.SQL.insert( this.tableName, data );
26
+ }
27
+
28
+ }
@@ -598,7 +598,10 @@ declare type Routes = {
598
598
  console.warn(e);
599
599
 
600
600
  if (request.accepts("html"))
601
- await response.runController(route, { message: e.message });
601
+ await response.runController(route, {
602
+ message: e.message,
603
+ type: e.constructor.name
604
+ });
602
605
  else if (request.accepts("json"))
603
606
  await response.json(e.message);
604
607
  else
@@ -47,17 +47,10 @@ export default class RequestValidator extends ServerSchemaValidator implements R
47
47
  const schema = fields instanceof Schema ? fields : new Schema(fields);
48
48
 
49
49
  // Les InputError seront propagées vers le middleware dédié à la gestion des erreurs
50
- const { values } = schema.validate(
51
- this.request.data,
52
- this.request.data,
53
- {},
54
- {
55
- debug: this.config.debug,
56
- throwError: true,
57
- validateDeps: false
58
- },
59
- []
60
- );
50
+ const values = schema.validate( this.request.data, {
51
+ debug: this.config.debug,
52
+ validateDeps: false
53
+ }, []);
61
54
 
62
55
  return values;
63
56
  }