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.
- package/package.json +2 -2
- package/src/client/assets/css/components/button.less +6 -10
- package/src/client/assets/css/components/card.less +1 -7
- package/src/client/assets/css/text/icons.less +8 -5
- package/src/client/assets/css/text/text.less +2 -3
- package/src/client/assets/css/theme.less +1 -1
- package/src/client/assets/css/utils/layouts.less +4 -0
- package/src/client/components/Form.ts +2 -1
- package/src/client/components/Select/ChoiceElement.tsx +20 -6
- package/src/client/components/Select/ChoiceSelector.tsx +3 -2
- package/src/client/components/Select/index.tsx +53 -21
- package/src/client/components/containers/Popover/index.tsx +4 -1
- package/src/client/components/data/Time.tsx +16 -12
- package/src/client/components/inputv3/base.tsx +1 -0
- package/src/client/components/inputv3/index.tsx +3 -1
- package/src/client/services/router/components/router.tsx +10 -8
- package/src/client/services/router/index.tsx +0 -0
- package/src/client/services/router/request/api.ts +9 -3
- package/src/common/data/dates.ts +20 -4
- package/src/common/data/objets.ts +17 -0
- package/src/common/validation/schema.ts +85 -91
- package/src/common/validation/validator.ts +4 -6
- package/src/common/validation/validators.ts +75 -72
- package/src/server/app/service/index.ts +1 -1
- package/src/server/app.tsconfig.json +1 -1
- package/src/server/services/database/connection.ts +3 -1
- package/src/server/services/database/index.ts +22 -22
- package/src/server/services/database/model.ts +28 -0
- package/src/server/services/router/index.ts +4 -1
- 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.
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
&.
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
183
|
+
margin: 1em 0;
|
|
185
184
|
}
|
|
186
185
|
|
|
187
186
|
h2 {
|
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
171
|
+
<div class="card col" style={{ width: '200px' }}>
|
|
160
172
|
|
|
161
173
|
<div class="col">
|
|
162
174
|
|
|
163
|
-
{
|
|
164
|
-
<
|
|
165
|
-
{
|
|
166
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
13
|
+
- TYPES
|
|
13
14
|
----------------------------------*/
|
|
15
|
+
export type { TDateInfo } from '@common/data/dates';
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 [
|
|
32
|
+
const [time, setTime] = React.useState<TDateInfo | null>( timeSince(since) );
|
|
29
33
|
|
|
30
34
|
React.useEffect(() => {
|
|
31
35
|
|
|
32
|
-
const textUpdate = setInterval(() =>
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
package/src/common/data/dates.ts
CHANGED
|
@@ -5,19 +5,35 @@ const timeAgo = new TimeAgo('en-US')
|
|
|
5
5
|
|
|
6
6
|
import dayjs from 'dayjs';
|
|
7
7
|
|
|
8
|
-
export
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
72
|
+
// Default options
|
|
73
73
|
opts = {
|
|
74
74
|
debug: false,
|
|
75
|
-
throwError:
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
135
|
-
|
|
158
|
+
dataToValidate: Partial<TDonnees>,
|
|
159
|
+
allData: TDonnees,
|
|
160
|
+
output: TObjetDonnees = {},
|
|
136
161
|
|
|
137
|
-
|
|
162
|
+
opts: TValidateOptions<TFields> = {},
|
|
163
|
+
chemin: string[] = []
|
|
138
164
|
|
|
139
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
88
|
-
|
|
96
|
+
if (subtype === undefined)
|
|
97
|
+
return items;
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
const validator = subtype.constructor === Object
|
|
100
|
+
? new Schema(subtype as TSchemaFields)
|
|
101
|
+
: subtype as Schema<{}> | Validator<any>;
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
190
|
+
new Validator<string>('url', (inputVal, options, path) => {
|
|
183
191
|
|
|
184
|
-
let val = this.string(opts).validate(inputVal,
|
|
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,
|
|
208
|
+
new Validator<string>('email', (inputVal, options, path) => {
|
|
201
209
|
|
|
202
|
-
let val = this.string(opts).validate(inputVal,
|
|
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,
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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<
|
|
428
|
+
): Promise<TData | TData[]> {
|
|
417
429
|
|
|
418
430
|
const table = this.database.getTable(path);
|
|
419
431
|
|
|
420
432
|
// Normalize data
|
|
421
|
-
|
|
433
|
+
let returnSingleResult = false;
|
|
434
|
+
if (!Array.isArray(data)) {
|
|
422
435
|
data = [data];
|
|
423
|
-
|
|
436
|
+
returnSingleResult = true;
|
|
437
|
+
} else if (data.length === 0) {
|
|
424
438
|
console.warn(LogPrefix, `Insert nothing in ${path}. Cancelled.`);
|
|
425
|
-
return
|
|
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
|
-
|
|
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, {
|
|
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
|
|
51
|
-
this.
|
|
52
|
-
|
|
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
|
}
|