5htp-core 0.2.3 → 0.2.4-1
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 +1 -1
- package/src/client/app/index.ts +1 -5
- package/src/client/assets/css/components.less +2 -2
- package/src/client/assets/css/utils/layouts.less +1 -1
- package/src/client/components/Dialog/card.tsx +1 -1
- package/src/client/components/Dialog/index.less +2 -2
- package/src/client/components/Form.ts +42 -11
- package/src/client/components/Select/index.tsx +159 -24
- package/src/client/components/dropdown/index.tsx +8 -13
- package/src/client/components/inputv3/base.tsx +12 -5
- package/src/client/components/inputv3/string/index.tsx +1 -1
- package/src/client/services/router/request/api.ts +3 -1
- package/src/common/validation/index.ts +2 -1
- package/src/common/validation/schema.ts +2 -2
- package/src/common/validation/validators.ts +21 -10
- package/src/server/app/index.ts +2 -0
- package/src/server/services/console/bugReporter.ts +2 -19
- package/src/server/services/database/connection.ts +8 -18
- package/src/server/services/database/index.ts +18 -6
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.2.
|
|
4
|
+
"version": "0.2.4-1",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
package/src/client/app/index.ts
CHANGED
|
@@ -102,11 +102,7 @@ export default abstract class Application {
|
|
|
102
102
|
})
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
public handleError( error: CoreError | Error, httpCode?: number )
|
|
106
|
-
|
|
107
|
-
/*console.error(`[api] Network error:`, e);
|
|
108
|
-
context.toast.error("Please check your internet connection and try again.", undefined, null, { autohide: false });*/
|
|
109
|
-
}
|
|
105
|
+
public abstract handleError( error: CoreError | Error, httpCode?: number );
|
|
110
106
|
|
|
111
107
|
public reportBug = (infos: TBugReportInfos) => fetch('/help/bug/gui', {
|
|
112
108
|
method: 'POST',
|
|
@@ -82,8 +82,7 @@
|
|
|
82
82
|
gap: @spacing;
|
|
83
83
|
z-index: @modal-zindex;
|
|
84
84
|
|
|
85
|
-
background: fade(#
|
|
86
|
-
backdrop-filter: blur(30px) saturate(2);
|
|
85
|
+
background: fade(#000, 20%);
|
|
87
86
|
border-radius: @radius;
|
|
88
87
|
|
|
89
88
|
// Desktop = vertically center the modal
|
|
@@ -109,6 +108,7 @@
|
|
|
109
108
|
max-height: 100vh;
|
|
110
109
|
box-shadow: none;
|
|
111
110
|
padding: @spacing * 2;
|
|
111
|
+
overflow-y: auto;
|
|
112
112
|
|
|
113
113
|
// Pas d'anim quand pas card,
|
|
114
114
|
// Car peut contenir bcp d'elemnts => performance
|
|
@@ -25,6 +25,13 @@ export type Form<TFormData extends {} = {}> = {
|
|
|
25
25
|
set: (data: Partial<TFormData>) => void,
|
|
26
26
|
submit: (additionnalData?: Partial<TFormData>) => Promise<any>,
|
|
27
27
|
fields: FieldsAttrs<TFormData>,
|
|
28
|
+
} & FormState
|
|
29
|
+
|
|
30
|
+
type FormState = {
|
|
31
|
+
isLoading: boolean,
|
|
32
|
+
errorsCount: number,
|
|
33
|
+
errors: {[fieldName: string]: string[]},
|
|
34
|
+
changed: boolean
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
/*----------------------------------
|
|
@@ -38,15 +45,16 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
|
|
|
38
45
|
const fields = React.useRef<FieldsAttrs<TFormData>>(null);
|
|
39
46
|
|
|
40
47
|
const [data, setData] = React.useState<TFormData>( options.data || {} );
|
|
41
|
-
const [state, setState] = React.useState({
|
|
48
|
+
const [state, setState] = React.useState<FormState>({
|
|
42
49
|
isLoading: false,
|
|
43
50
|
errorsCount: 0,
|
|
44
51
|
errors: {},
|
|
45
|
-
|
|
52
|
+
changed: false
|
|
46
53
|
});
|
|
47
54
|
|
|
55
|
+
// Validate data when it changes
|
|
48
56
|
React.useEffect(() => {
|
|
49
|
-
state.
|
|
57
|
+
state.changed && validate(data);
|
|
50
58
|
}, [data]);
|
|
51
59
|
|
|
52
60
|
/*----------------------------------
|
|
@@ -58,13 +66,10 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
|
|
|
58
66
|
|
|
59
67
|
// Update errors
|
|
60
68
|
if (validated.nbErreurs !== state.errorsCount) {
|
|
61
|
-
|
|
62
|
-
setState( old => ({
|
|
63
|
-
...old,
|
|
69
|
+
rebuildFieldsAttrs({
|
|
64
70
|
errorsCount: validated.nbErreurs,
|
|
65
71
|
errors: validated.erreurs,
|
|
66
|
-
|
|
67
|
-
}));
|
|
72
|
+
});
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
return validated;
|
|
@@ -84,14 +89,39 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
|
|
|
84
89
|
return options.submit(validated.values);
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
const rebuildFieldsAttrs = (newState: Partial<FormState> = {}) => {
|
|
93
|
+
// Force rebuilding the fields definition on the next state change
|
|
94
|
+
fields.current = null;
|
|
95
|
+
// Force state change
|
|
96
|
+
setState( old => ({
|
|
97
|
+
...old,
|
|
98
|
+
...newState,
|
|
99
|
+
changed: true
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Rebuild the fields attrs when the schema changes
|
|
104
|
+
if (fields.current === null || Object.keys(schema).join(',') !== Object.keys(fields.current).join(',')){
|
|
88
105
|
fields.current = {}
|
|
89
106
|
for (const fieldName in schema.fields) {
|
|
90
107
|
fields.current[fieldName] = {
|
|
91
108
|
|
|
92
109
|
// Value control
|
|
93
110
|
value: data[fieldName],
|
|
94
|
-
onChange: (val) =>
|
|
111
|
+
onChange: (val) => {
|
|
112
|
+
setState( old => ({
|
|
113
|
+
...old,
|
|
114
|
+
changed: true
|
|
115
|
+
}));
|
|
116
|
+
setData( old => {
|
|
117
|
+
return {
|
|
118
|
+
...old,
|
|
119
|
+
[fieldName]: typeof val === 'function'
|
|
120
|
+
? val(old[fieldName])
|
|
121
|
+
: val
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
},
|
|
95
125
|
|
|
96
126
|
// Submit on press enter
|
|
97
127
|
onKeyDown: e => {
|
|
@@ -102,7 +132,8 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
|
|
|
102
132
|
|
|
103
133
|
// Error
|
|
104
134
|
errors: state.errors[ fieldName ],
|
|
105
|
-
required: schema.fields[ fieldName ].options?.opt !== true
|
|
135
|
+
required: schema.fields[ fieldName ].options?.opt !== true,
|
|
136
|
+
validator: schema.fields[ fieldName ]
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
}
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
// Npm
|
|
6
6
|
import React from 'react';
|
|
7
|
+
import type { ComponentChild } from 'preact';
|
|
8
|
+
import type { StateUpdater } from 'preact/hooks';
|
|
7
9
|
|
|
8
10
|
// Core
|
|
9
11
|
import Button from '@client/components/button';
|
|
12
|
+
import String from '@client/components/inputv3/string';
|
|
10
13
|
import Dropdown, { TDialogControls, Props as DropdownProps } from '@client/components/dropdown';
|
|
11
14
|
|
|
12
15
|
/*----------------------------------
|
|
@@ -17,42 +20,174 @@ import Dropdown, { TDialogControls, Props as DropdownProps } from '@client/compo
|
|
|
17
20
|
- TYPES
|
|
18
21
|
----------------------------------*/
|
|
19
22
|
|
|
20
|
-
type
|
|
23
|
+
export type Choice = { label: ComponentChild, value: string }
|
|
21
24
|
|
|
22
|
-
type
|
|
25
|
+
export type Choices = Choice[]
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
type ChoicesFunc = (search: string) => Promise<Choices>
|
|
28
|
+
|
|
29
|
+
type SelectorProps = (
|
|
30
|
+
{
|
|
31
|
+
multiple: true,
|
|
32
|
+
value?: Choice[],
|
|
33
|
+
onChange: StateUpdater<Choice[]>,
|
|
34
|
+
validator?: ArrayValidator
|
|
35
|
+
}
|
|
36
|
+
|
|
|
37
|
+
{
|
|
38
|
+
multiple?: false,
|
|
39
|
+
value?: Choice,
|
|
40
|
+
onChange: StateUpdater<Choice>,
|
|
41
|
+
validator?: StringValidator
|
|
42
|
+
}
|
|
43
|
+
) & {
|
|
44
|
+
choices: Choices | ChoicesFunc,
|
|
45
|
+
enableSearch?: boolean,
|
|
46
|
+
inline?: boolean,
|
|
47
|
+
errors?: string[],
|
|
48
|
+
required?: boolean,
|
|
49
|
+
noneSelection?: false | string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Props = DropdownProps & SelectorProps & {
|
|
25
53
|
title: string,
|
|
26
|
-
choices: Choices,
|
|
27
|
-
value?: string,
|
|
28
|
-
onChange: (value: string) => void,
|
|
29
|
-
search?: true | SearchResultsFunction
|
|
30
54
|
}
|
|
31
55
|
|
|
32
56
|
/*----------------------------------
|
|
33
57
|
- COMONENT
|
|
34
58
|
----------------------------------*/
|
|
59
|
+
export default ({
|
|
60
|
+
title, choices: initChoices, errors, validator, required, noneSelection, enableSearch, value: current,
|
|
61
|
+
onChange, inline, multiple,
|
|
62
|
+
...props
|
|
63
|
+
}: Props) => {
|
|
35
64
|
|
|
36
|
-
export default ({ title, choices, value, onChange, ...dropDownProps }: Props) => {
|
|
37
65
|
const refModal = React.useRef<TDialogControls>(null);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
|
|
67
|
+
/*----------------------------------
|
|
68
|
+
- INIT
|
|
69
|
+
----------------------------------*/
|
|
70
|
+
|
|
71
|
+
const choicesViaFunc = typeof initChoices === 'function';
|
|
72
|
+
if (choicesViaFunc && enableSearch === undefined)
|
|
73
|
+
enableSearch = true;
|
|
74
|
+
|
|
75
|
+
const [search, setSearch] = React.useState<{
|
|
76
|
+
keywords: string,
|
|
77
|
+
loading: boolean
|
|
78
|
+
}>({
|
|
79
|
+
keywords: '',
|
|
80
|
+
loading: choicesViaFunc
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const [choices, setChoices] = React.useState<Choices>([]);
|
|
84
|
+
|
|
85
|
+
/*----------------------------------
|
|
86
|
+
- ACTIONS
|
|
87
|
+
----------------------------------*/
|
|
88
|
+
React.useEffect(() => {
|
|
89
|
+
if (choicesViaFunc) {
|
|
90
|
+
initChoices(search.keywords).then((searchResults) => {
|
|
91
|
+
setSearch(s => ({ ...s, loading: false }))
|
|
92
|
+
setChoices(searchResults);
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}, [initChoices, search.keywords]);
|
|
96
|
+
|
|
97
|
+
const currentList: Choice[] = current === undefined
|
|
98
|
+
? []
|
|
99
|
+
: (Array.isArray(current) ? current : [current]);
|
|
100
|
+
|
|
101
|
+
/*----------------------------------
|
|
102
|
+
- RENDER
|
|
103
|
+
----------------------------------*/
|
|
104
|
+
|
|
105
|
+
const selector = (
|
|
106
|
+
<div class={(inline ? '' : 'card ') + "col"}>
|
|
107
|
+
|
|
108
|
+
{enableSearch && (
|
|
109
|
+
<String icon="search"
|
|
110
|
+
title="Search"
|
|
111
|
+
value={search.keywords}
|
|
112
|
+
onChange={keywords => setSearch(s => ({ ...s, loading: true, keywords }))}
|
|
113
|
+
iconR={'spin'}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{currentList.length !== 0 && (
|
|
118
|
+
<ul class="col menu">
|
|
119
|
+
{currentList.map(choice => (
|
|
120
|
+
<Button size="s" onClick={() => {
|
|
121
|
+
onChange( current => multiple
|
|
122
|
+
? current.filter(c => c.value !== choice.value)
|
|
123
|
+
: undefined
|
|
124
|
+
);
|
|
125
|
+
}} suffix={<i src="check" class="fg primary" />}>
|
|
126
|
+
{choice.label}
|
|
48
127
|
</Button>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{choices === null ? (
|
|
133
|
+
<div class="row h-3 al-center">
|
|
134
|
+
<i src="spin" />
|
|
135
|
+
</div>
|
|
136
|
+
) : (
|
|
137
|
+
<ul class="col menu">
|
|
138
|
+
{choices.map( choice => {
|
|
139
|
+
|
|
140
|
+
const isCurrent = currentList.some(c => c.value === choice.value);
|
|
53
141
|
|
|
54
|
-
|
|
142
|
+
return !isCurrent && (
|
|
143
|
+
<li>
|
|
144
|
+
<Button size="s" onClick={() => {
|
|
145
|
+
onChange( current => {
|
|
146
|
+
return multiple
|
|
147
|
+
? [...(current || []), choice]
|
|
148
|
+
: choice
|
|
149
|
+
});
|
|
150
|
+
}}>
|
|
151
|
+
{/*search.keywords ? (
|
|
152
|
+
<span>
|
|
153
|
+
|
|
154
|
+
<strong>{search.keywords}</strong>{choice.label.slice( search.keywords.length )}
|
|
155
|
+
|
|
156
|
+
</span>
|
|
157
|
+
) : */choice.label}
|
|
158
|
+
</Button>
|
|
159
|
+
</li>
|
|
160
|
+
)
|
|
161
|
+
})}
|
|
55
162
|
|
|
56
|
-
|
|
163
|
+
{((!required || !validator?.options.min) && noneSelection) && (
|
|
164
|
+
<li>
|
|
165
|
+
<Button size="s" onClick={() => onChange(multiple ? [] : undefined)}
|
|
166
|
+
suffix={(current === undefined || (multiple && current.length === 0)) && <i src="check" class="fg primary" />}>
|
|
167
|
+
{noneSelection}
|
|
168
|
+
</Button>
|
|
169
|
+
</li>
|
|
170
|
+
)}
|
|
171
|
+
</ul>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
57
174
|
)
|
|
175
|
+
|
|
176
|
+
return <>
|
|
177
|
+
{inline ? selector : (
|
|
178
|
+
<Dropdown {...props} content={selector} iconR="chevron-down" refModal={refModal}>
|
|
179
|
+
|
|
180
|
+
{currentList.length === 0
|
|
181
|
+
? title
|
|
182
|
+
: currentList.map(choice => choice.label).join(', ')}
|
|
183
|
+
|
|
184
|
+
</Dropdown>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{errors?.length && (
|
|
188
|
+
<div class="fg error txt-left">
|
|
189
|
+
{errors.join('. ')}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</>
|
|
58
193
|
}
|
|
@@ -9,15 +9,14 @@ import { ComponentChild, RefObject } from 'preact';
|
|
|
9
9
|
// Core
|
|
10
10
|
import Button, { Props as ButtonProps } from '../button';
|
|
11
11
|
import { TDialogControls } from '../Dialog/Manager';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Libs
|
|
15
|
-
import useContexte from '@/client/context';
|
|
12
|
+
import Popover from '../containers/Popover';
|
|
16
13
|
|
|
17
14
|
/*----------------------------------
|
|
18
15
|
- TYPES
|
|
19
16
|
----------------------------------*/
|
|
20
17
|
|
|
18
|
+
export type { TDialogControls } from '../Dialog/Manager';
|
|
19
|
+
|
|
21
20
|
export type Props = ButtonProps & {
|
|
22
21
|
content: ComponentChild,
|
|
23
22
|
refModal?: RefObject<TDialogControls>
|
|
@@ -28,23 +27,19 @@ export type Props = ButtonProps & {
|
|
|
28
27
|
----------------------------------*/
|
|
29
28
|
export default (props: Props) => {
|
|
30
29
|
|
|
31
|
-
const { modal } = useContexte();
|
|
32
|
-
|
|
33
30
|
let {
|
|
34
31
|
content,
|
|
35
32
|
refModal,
|
|
36
33
|
...buttonProps
|
|
37
34
|
} = props;
|
|
38
35
|
|
|
39
|
-
const
|
|
36
|
+
const popoverState = React.useState(false);
|
|
40
37
|
|
|
41
|
-
const
|
|
42
|
-
const modalInstance = modal.show(() => content);
|
|
43
|
-
if (refModal)
|
|
44
|
-
refModal.current = modalInstance;
|
|
45
|
-
}
|
|
38
|
+
const refButton = React.useRef<HTMLElement>(null);
|
|
46
39
|
|
|
47
40
|
return (
|
|
48
|
-
<
|
|
41
|
+
<Popover content={content} state={popoverState}>
|
|
42
|
+
<Button {...buttonProps} refElem={refButton} />
|
|
43
|
+
</Popover>
|
|
49
44
|
)
|
|
50
45
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
// Npm
|
|
6
6
|
import React from 'react';
|
|
7
|
-
import {
|
|
7
|
+
import type { StateUpdater } from 'preact/hooks';
|
|
8
8
|
|
|
9
9
|
// Core libs
|
|
10
10
|
import { useState } from '@client/hooks';
|
|
@@ -20,7 +20,7 @@ export type InputBaseProps<TValue> = {
|
|
|
20
20
|
errors?: string[],
|
|
21
21
|
|
|
22
22
|
value: TValue,
|
|
23
|
-
onChange?:
|
|
23
|
+
onChange?: StateUpdater<TValue>,
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export type TInputState<TValue> = {
|
|
@@ -28,6 +28,7 @@ export type TInputState<TValue> = {
|
|
|
28
28
|
fieldProps: {[key: string]: any},
|
|
29
29
|
valueSource: 'internal'|'external',
|
|
30
30
|
focus: boolean,
|
|
31
|
+
changed: boolean,
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/*----------------------------------
|
|
@@ -47,12 +48,18 @@ export function useInput<TValue>(
|
|
|
47
48
|
value: externalValue !== undefined ? externalValue : defaultValue,
|
|
48
49
|
valueSource: 'external',
|
|
49
50
|
fieldProps: {},
|
|
50
|
-
focus: false
|
|
51
|
+
focus: false,
|
|
52
|
+
changed: false
|
|
51
53
|
});
|
|
52
54
|
|
|
53
|
-
const setValue = (value: TValue) => setState({ value, valueSource: 'internal' });
|
|
55
|
+
const setValue = (value: TValue) => setState({ value, valueSource: 'internal', changed: true });
|
|
54
56
|
|
|
55
57
|
const commitValue = () => {
|
|
58
|
+
|
|
59
|
+
// Avoid to change parent component state at first render
|
|
60
|
+
if (state.changed === false)
|
|
61
|
+
return;
|
|
62
|
+
|
|
56
63
|
console.log(`[input] Commit value:`, state.value);
|
|
57
64
|
if (onChange !== undefined)
|
|
58
65
|
onChange(state.value);
|
|
@@ -63,7 +70,7 @@ export function useInput<TValue>(
|
|
|
63
70
|
|
|
64
71
|
if (externalValue !== undefined && externalValue !== state.value) {
|
|
65
72
|
console.log("External value change", externalValue);
|
|
66
|
-
setState({ value: externalValue, valueSource: 'external' })
|
|
73
|
+
setState({ value: externalValue, valueSource: 'external', changed: true })
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
}, [externalValue]);
|
|
@@ -127,7 +127,7 @@ export default class ApiClient implements ApiClientService {
|
|
|
127
127
|
|
|
128
128
|
return await this.fetch<TData>(method, path, data, options).catch((e) => {
|
|
129
129
|
this.app.handleError(e);
|
|
130
|
-
throw e;
|
|
130
|
+
throw e; // Throw to break the loop
|
|
131
131
|
})
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -158,6 +158,8 @@ export default class ApiClient implements ApiClientService {
|
|
|
158
158
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
+
// Errors will be catched in the caller
|
|
162
|
+
|
|
161
163
|
return { ...alreadyLoadedData, ...fetchedData }
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -93,7 +93,7 @@ export default class Schema<TFields extends TSchemaFields> {
|
|
|
93
93
|
opts.debug && console.warn(LogPrefix, '[' + champ + ']', 'Exclusion (pas présent dans le schéma)');
|
|
94
94
|
continue;
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
const cheminA = [...chemin, champ]
|
|
98
98
|
const cheminAstr = cheminA.join('.')
|
|
99
99
|
|
|
@@ -178,7 +178,7 @@ export default class Schema<TFields extends TSchemaFields> {
|
|
|
178
178
|
if (nbErreurs !== 0 && opts.throwError === true) {
|
|
179
179
|
throw new InputErrorSchema(erreurs);
|
|
180
180
|
}
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
opts.debug && console.log(LogPrefix, '', dataToValidate, '=>', output);
|
|
183
183
|
|
|
184
184
|
return {
|
|
@@ -63,8 +63,10 @@ export default class SchemaValidator {
|
|
|
63
63
|
return val;
|
|
64
64
|
}, opts)
|
|
65
65
|
|
|
66
|
-
public array = (subtype?: Validator<any>, { choice, ...opts }: TValidator<any[]> & {
|
|
67
|
-
choice?: any[]
|
|
66
|
+
public array = (subtype?: Validator<any>, { choice, min, max, ...opts }: TValidator<any[]> & {
|
|
67
|
+
choice?: any[],
|
|
68
|
+
min?: number,
|
|
69
|
+
max?: number
|
|
68
70
|
} = {}) => {
|
|
69
71
|
|
|
70
72
|
if (subtype !== undefined)
|
|
@@ -72,12 +74,17 @@ export default class SchemaValidator {
|
|
|
72
74
|
|
|
73
75
|
return new Validator<any[]>('array', (items, input, output, corriger) => {
|
|
74
76
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
+
// Type
|
|
77
78
|
if (!Array.isArray(items))
|
|
78
|
-
throw new InputError("This value must be
|
|
79
|
+
throw new InputError("This value must be a list.");
|
|
80
|
+
|
|
81
|
+
// Items number
|
|
82
|
+
if ((min !== undefined && items.length < min))
|
|
83
|
+
throw new InputError(`Please select at least ${min} items.`);
|
|
84
|
+
if ((max !== undefined && items.length > max))
|
|
85
|
+
throw new InputError(`Please select maximum ${max} items.`);
|
|
79
86
|
|
|
80
|
-
// Verif
|
|
87
|
+
// Verif each item
|
|
81
88
|
if (subtype !== undefined) {
|
|
82
89
|
if (false/*subtype instanceof Schema*/) {
|
|
83
90
|
|
|
@@ -85,9 +92,9 @@ export default class SchemaValidator {
|
|
|
85
92
|
|
|
86
93
|
} else {
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
items = items.map( item =>
|
|
96
|
+
subtype.validate( item, items, items, corriger )
|
|
97
|
+
)
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
|
|
@@ -101,7 +108,7 @@ export default class SchemaValidator {
|
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
public choice = (values: any[], opts: TValidator<any> & {} = {}) =>
|
|
104
|
-
new Validator<any>('
|
|
111
|
+
new Validator<any>('choice', (val, input, output) => {
|
|
105
112
|
|
|
106
113
|
if (!values.includes(val))
|
|
107
114
|
throw new InputError("Invalid value. Must be: " + values.join(', '));
|
|
@@ -116,6 +123,10 @@ export default class SchemaValidator {
|
|
|
116
123
|
public string = ({ min, max, ...opts }: TValidator<string> & { min?: number, max?: number } = {}) =>
|
|
117
124
|
new Validator<string>('string', (val, input, output, corriger?: boolean) => {
|
|
118
125
|
|
|
126
|
+
// Choice value from Select component
|
|
127
|
+
if (typeof val === 'object' && val.value)
|
|
128
|
+
val = val.value;
|
|
129
|
+
|
|
119
130
|
if (val === '')
|
|
120
131
|
return undefined;
|
|
121
132
|
else if (typeof val === 'number')
|
package/src/server/app/index.ts
CHANGED
|
@@ -104,6 +104,8 @@ export default abstract class Application extends Service<Config, Hooks, /* TODO
|
|
|
104
104
|
const configParser = new ConfigParser( this.path.root );
|
|
105
105
|
this.env = configParser.env();
|
|
106
106
|
this.identity = configParser.identity();
|
|
107
|
+
|
|
108
|
+
console.log(`[boot] Environment:`, this.env);
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
/*----------------------------------
|
|
@@ -170,7 +170,7 @@ export default class BugReporter {
|
|
|
170
170
|
channelType,
|
|
171
171
|
channelId,
|
|
172
172
|
// User
|
|
173
|
-
user: request?.user?.
|
|
173
|
+
user: request?.user?.email,
|
|
174
174
|
ip: request?.ip,
|
|
175
175
|
// Error
|
|
176
176
|
error,
|
|
@@ -180,25 +180,8 @@ export default class BugReporter {
|
|
|
180
180
|
|
|
181
181
|
await this.sendToTransporters(bugReport);
|
|
182
182
|
|
|
183
|
-
// TODO: Move on App side
|
|
184
|
-
/*if (app.isLoaded('sql'))
|
|
185
|
-
// Memorize
|
|
186
|
-
$.sql.insert('BugServer', {
|
|
187
|
-
// Context
|
|
188
|
-
hash: hash,
|
|
189
|
-
date: now,
|
|
190
|
-
channelType,
|
|
191
|
-
channelId,
|
|
192
|
-
// User
|
|
193
|
-
user: request?.user?.name,
|
|
194
|
-
ip: request?.ip,
|
|
195
|
-
// Error
|
|
196
|
-
stacktrace: error.stack || error.message,
|
|
197
|
-
logs: logsHtml
|
|
198
|
-
});*/
|
|
199
|
-
|
|
200
183
|
// Update error message
|
|
201
|
-
error.message = "
|
|
184
|
+
error.message = "We encountered an internal error, and our team has just been notified. Sorry for the inconvenience.";
|
|
202
185
|
}
|
|
203
186
|
|
|
204
187
|
public async application( report: AppBugInfos ) {
|
|
@@ -28,18 +28,10 @@ const LogPrefix = '[database][connection]';
|
|
|
28
28
|
|
|
29
29
|
export type DatabaseServiceConfig = {
|
|
30
30
|
list: string[],
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
password: string,
|
|
36
|
-
},
|
|
37
|
-
prod: {
|
|
38
|
-
host: string,
|
|
39
|
-
port: number,
|
|
40
|
-
login: string,
|
|
41
|
-
password: string,
|
|
42
|
-
}
|
|
31
|
+
host: string,
|
|
32
|
+
port: number,
|
|
33
|
+
login: string,
|
|
34
|
+
password: string,
|
|
43
35
|
}
|
|
44
36
|
|
|
45
37
|
export type THooks = {
|
|
@@ -113,15 +105,13 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
|
|
|
113
105
|
|
|
114
106
|
console.info(LogPrefix, `Connecting to databases ...`);
|
|
115
107
|
|
|
116
|
-
const creds = this.config[ this.app.env.profile ];
|
|
117
|
-
|
|
118
108
|
return await mysql.createPool({
|
|
119
109
|
|
|
120
110
|
// Identification
|
|
121
|
-
host:
|
|
122
|
-
port:
|
|
123
|
-
user:
|
|
124
|
-
password:
|
|
111
|
+
host: this.config.host,
|
|
112
|
+
port: this.config.port,
|
|
113
|
+
user: this.config.login,
|
|
114
|
+
password: this.config.password,
|
|
125
115
|
database: this.config.list[0],
|
|
126
116
|
|
|
127
117
|
// Pool
|
|
@@ -279,7 +279,7 @@ export default class SQL extends Service<Config, Hooks, Application> {
|
|
|
279
279
|
public update<TData extends TObjetDonnees>(
|
|
280
280
|
tableName: string,
|
|
281
281
|
data: TData[],
|
|
282
|
-
where
|
|
282
|
+
where?: (keyof TData)[],
|
|
283
283
|
opts?: TUpdateQueryOptions<TData>
|
|
284
284
|
);
|
|
285
285
|
|
|
@@ -287,30 +287,42 @@ export default class SQL extends Service<Config, Hooks, Application> {
|
|
|
287
287
|
public update<TData extends TObjetDonnees>(
|
|
288
288
|
tableName: string,
|
|
289
289
|
data: TData,
|
|
290
|
-
where
|
|
290
|
+
where?: (keyof TData)[] | TObjetDonnees,
|
|
291
291
|
opts?: TUpdateQueryOptions<TData>
|
|
292
292
|
);
|
|
293
293
|
|
|
294
294
|
public update<TData extends TObjetDonnees>(...args: [
|
|
295
295
|
tableName: string,
|
|
296
296
|
data: TData[],
|
|
297
|
-
where
|
|
297
|
+
where?: (keyof TData)[],
|
|
298
298
|
opts?: TUpdateQueryOptions<TData>
|
|
299
299
|
] | [
|
|
300
300
|
tableName: string,
|
|
301
301
|
data: TData,
|
|
302
|
-
where
|
|
302
|
+
where?: (keyof TData)[] | TObjetDonnees,
|
|
303
303
|
opts?: TUpdateQueryOptions<TData>
|
|
304
|
-
]) {
|
|
304
|
+
]): Promise<ResultSetHeader> {
|
|
305
305
|
|
|
306
306
|
let [tableName, data, where, opts] = args;
|
|
307
307
|
|
|
308
308
|
// Multiple updates in one
|
|
309
309
|
if (Array.isArray( data ))
|
|
310
310
|
return this.database.query(
|
|
311
|
-
data.map(record =>
|
|
311
|
+
data.map( record =>
|
|
312
|
+
this.update(tableName, record, where, { ...opts, returnQuery: true })
|
|
313
|
+
).join(';\n')
|
|
312
314
|
)
|
|
313
315
|
|
|
316
|
+
// Automatic where based on pks
|
|
317
|
+
if (where === undefined) {
|
|
318
|
+
|
|
319
|
+
const tableMetas = this.database.getTable(tableName);
|
|
320
|
+
if (tableMetas.pk.length === 0)
|
|
321
|
+
throw new Error(`Tried to build the where condition based on the pks list, but no pk is attached to this table ${tableMetas.chemin}`);
|
|
322
|
+
|
|
323
|
+
where = tableMetas.pk;
|
|
324
|
+
}
|
|
325
|
+
|
|
314
326
|
// No condition specified = use the pks
|
|
315
327
|
if (Array.isArray(where)) {
|
|
316
328
|
const whereColNames = where;
|