5htp-core 0.1.2 → 0.2.0
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/changelog.md +5 -0
- package/doc/TODO.md +71 -0
- package/package.json +5 -4
- package/src/client/{App.tsx → app/component.tsx} +15 -8
- package/src/client/app/index.ts +128 -0
- package/src/client/app/service.ts +34 -0
- package/src/client/app.tsconfig.json +0 -4
- package/src/client/assets/css/medias.less +14 -0
- package/src/client/components/Card/index.tsx +2 -2
- package/src/client/components/Dialog/Manager.tsx +39 -12
- package/src/client/components/Form/index.tsx +1 -1
- package/src/client/components/button.tsx +2 -2
- package/src/client/components/containers/Popover/index.tsx +1 -1
- package/src/client/components/data/spintext/index.tsx +1 -1
- package/src/client/components/dropdown/index.tsx +1 -1
- package/src/client/components/index.ts +8 -0
- package/src/client/components/input/BaseV2/index.tsx +1 -1
- package/src/client/components/input/UploadImage/index.tsx +1 -1
- package/src/client/hooks/index.ts +5 -0
- package/src/client/hooks/useState/index.tsx +2 -2
- package/src/client/hooks.ts +22 -0
- package/src/client/index.ts +5 -0
- package/src/client/pages/_layout/landing/index.tsx +0 -2
- package/src/client/pages/_messages/400.tsx +2 -2
- package/src/client/pages/_messages/401.tsx +2 -2
- package/src/client/pages/_messages/403.tsx +2 -2
- package/src/client/pages/_messages/404.tsx +2 -2
- package/src/client/pages/_messages/500.tsx +2 -2
- package/src/client/pages/bug.tsx +1 -1
- package/src/client/pages/useHeader.tsx +1 -1
- package/src/client/{context/captcha.ts → services/captcha/index.ts} +0 -0
- package/src/client/services/metrics/index.ts +37 -0
- package/src/client/{router → services/router/components}/Link.tsx +1 -1
- package/src/client/services/router/components/Page.tsx +59 -0
- package/src/client/{router/component.tsx → services/router/components/router.tsx} +43 -74
- package/src/client/services/router/index.tsx +448 -0
- package/src/client/services/router/request/api.ts +229 -0
- package/src/client/{router → services/router}/request/history.ts +0 -0
- package/src/client/services/router/request/index.ts +52 -0
- package/src/client/services/router/response/index.tsx +107 -0
- package/src/client/services/router/response/page.ts +95 -0
- package/src/client/{context/socket.ts → services/socket/index.ts} +2 -2
- package/src/client/utils/dom.ts +1 -1
- package/src/common/app/index.ts +9 -0
- package/src/common/data/chaines/index.ts +9 -6
- package/src/common/data/input/validate.ts +3 -166
- package/src/common/data/objets.ts +25 -0
- package/src/common/data/tableaux.ts +8 -0
- package/src/common/errors/index.ts +3 -1
- package/src/common/router/index.ts +67 -88
- package/src/common/router/layouts.ts +50 -0
- package/src/common/router/register.ts +62 -0
- package/src/common/router/request/api.ts +72 -0
- package/src/common/router/request/index.ts +31 -0
- package/src/common/router/{response.ts → response/index.ts} +9 -13
- package/src/common/router/response/page.ts +40 -56
- package/src/common/validation/index.ts +3 -0
- package/src/common/validation/schema.ts +184 -0
- package/src/common/validation/validator.ts +88 -0
- package/src/common/validation/validators.ts +313 -0
- package/src/server/app/config.ts +9 -27
- package/src/server/app/index.ts +81 -124
- package/src/server/app/service.ts +98 -0
- package/src/server/app.tsconfig.json +0 -8
- package/src/server/error/index.ts +13 -0
- package/src/server/index.ts +5 -0
- package/src/server/patch.ts +0 -6
- package/src/server/{data/Cache.ts → services/cache/index.ts} +79 -47
- package/src/server/services/console/bugReporter.ts +26 -16
- package/src/server/services/console/index.ts +59 -51
- package/src/server/services/cron/index.ts +12 -26
- package/src/server/services/database/bucket.ts +40 -0
- package/src/server/services/database/connection.ts +206 -75
- package/src/server/services/database/datatypes.ts +63 -40
- package/src/server/services/database/index.ts +295 -272
- package/src/server/services/database/metas.ts +246 -135
- package/src/server/services/database/stats.ts +151 -126
- package/src/server/services/email/index.ts +28 -52
- package/src/server/services/{router/request/services → metrics}/detect.ts +8 -10
- package/src/server/services/{router/request/services/tracking.ts → metrics/index.ts} +68 -45
- package/src/server/services/{http → router/http}/index.ts +28 -70
- package/src/server/services/{http → router/http}/multipart.ts +0 -0
- package/src/server/services/{http → router/http}/session.ts.old +0 -0
- package/src/server/services/router/index.ts +273 -203
- package/src/server/services/router/request/api.ts +73 -0
- package/src/server/services/router/request/index.ts +16 -97
- package/src/server/services/router/request/service.ts +21 -0
- package/src/server/services/router/response/index.ts +125 -64
- package/src/server/services/router/response/{filter → mask}/Filter.ts +0 -0
- package/src/server/services/router/response/{filter → mask}/index.ts +0 -2
- package/src/server/services/router/response/{filter → mask}/selecteurs.ts +0 -0
- package/src/server/services/router/response/page/document.tsx +194 -0
- package/src/server/services/router/response/page/index.tsx +157 -0
- package/src/server/{libs/pages → services/router/response/page}/schemaGenerator.ts +0 -0
- package/src/server/services/router/service.ts +48 -0
- package/src/server/services/schema/index.ts +47 -0
- package/src/server/services/schema/request.ts +55 -0
- package/src/server/services/schema/router.ts +33 -0
- package/src/server/services/socket/index.ts +38 -43
- package/src/server/services/socket/scope.ts +6 -4
- package/src/server/services/users/index.ts +203 -0
- package/src/server/services/{auth/base.ts → users/old.ts} +28 -112
- package/src/server/services/users/router/index.ts +72 -0
- package/src/server/services/users/router/request.ts +49 -0
- package/src/types/aliases.d.ts +43 -2
- package/templates/composant.tsx +1 -1
- package/templates/modal.tsx +1 -1
- package/templates/page.tsx +1 -1
- package/tsconfig.common.json +0 -4
- package/src/client/context/api.ts +0 -92
- package/src/client/context/index.ts +0 -246
- package/src/client/index.tsx +0 -129
- package/src/client/router/index.ts +0 -286
- package/src/client/router/request/index.ts +0 -106
- package/src/client/router/response/index.ts +0 -38
- package/src/client/router/route.ts +0 -75
- package/src/common/data/input/validators/basic.ts +0 -299
- package/src/common/data/input/validators/build.ts +0 -63
- package/src/common/router/request.ts +0 -83
- package/src/server/data/ApiClient.ts +0 -119
- package/src/server/data/input.ts +0 -41
- package/src/server/libs/pages/document.static.tsx +0 -41
- package/src/server/libs/pages/document.tsx +0 -203
- package/src/server/libs/pages/render.tsx +0 -90
- package/src/server/routes/auth.ts +0 -151
- package/src/server/services/redis/index.ts +0 -71
- package/src/server/services/router/request/services/auth.ts +0 -177
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import ReactDOM from 'react-dom';
|
|
8
|
+
|
|
9
|
+
// Core
|
|
10
|
+
import type {
|
|
11
|
+
default as ServerRouter,
|
|
12
|
+
Request as ServerRequest,
|
|
13
|
+
Response as ServerResponse
|
|
14
|
+
} from '@server/services/router';
|
|
15
|
+
import type { TBasicSSrData } from '@server/services/router/response';
|
|
16
|
+
|
|
17
|
+
import { Erreur } from '@common/errors';
|
|
18
|
+
import BaseRouter, {
|
|
19
|
+
defaultOptions, TRoute, TErrorRoute, TClientOrServerContext, TRouteModule
|
|
20
|
+
} from '@common/router'
|
|
21
|
+
import { getRegisterPageArgs, buildRegex } from '@common/router/register';
|
|
22
|
+
import { TFetcherList } from '@common/router/request/api';
|
|
23
|
+
import type { TFrontRenderer, TDataProvider } from '@common/router/response/page';
|
|
24
|
+
|
|
25
|
+
import App from '@client/app/component';
|
|
26
|
+
import type ClientApplication from '@client/app';
|
|
27
|
+
import Service from '@client/app/service';
|
|
28
|
+
|
|
29
|
+
// Specific
|
|
30
|
+
import ClientRequest from './request';
|
|
31
|
+
import { location, history } from './request/history';
|
|
32
|
+
import ClientResponse from './response';
|
|
33
|
+
import ClientPage from './response/page';
|
|
34
|
+
|
|
35
|
+
// Routes (import __register)
|
|
36
|
+
import * as coreRoutes from '@client/pages/**/*.tsx';
|
|
37
|
+
import * as appRoutes from '@/client/pages/**/*.tsx';
|
|
38
|
+
|
|
39
|
+
/*----------------------------------
|
|
40
|
+
- CONFIG
|
|
41
|
+
----------------------------------*/
|
|
42
|
+
|
|
43
|
+
const debug = true;
|
|
44
|
+
const LogPrefix = '[router]'
|
|
45
|
+
|
|
46
|
+
/*----------------------------------
|
|
47
|
+
- TYPES
|
|
48
|
+
----------------------------------*/
|
|
49
|
+
|
|
50
|
+
// Client router can handle Client requests AND Server requests (for pages only)
|
|
51
|
+
export type { default as ClientResponse, TRouterContext } from "./response";
|
|
52
|
+
export { Link } from './components/Link';
|
|
53
|
+
|
|
54
|
+
export type Router = ClientRouter | ServerRouter;
|
|
55
|
+
|
|
56
|
+
export type Request = ClientRequest<ClientRouter> | ServerRequest<ServerRouter>;
|
|
57
|
+
|
|
58
|
+
export type Response = ClientResponse<ClientRouter> | ServerResponse<ServerRouter>;
|
|
59
|
+
|
|
60
|
+
/*----------------------------------
|
|
61
|
+
- TYPES: ROUTES LOADING
|
|
62
|
+
----------------------------------*/
|
|
63
|
+
|
|
64
|
+
export type TRegisterPageArgs<TProvidedData extends TFetcherList = {}, TRouter extends Router = Router> = ([
|
|
65
|
+
path: string,
|
|
66
|
+
controller: TDataProvider<TProvidedData> | null,
|
|
67
|
+
renderer: TFrontRenderer<TProvidedData>
|
|
68
|
+
] | [
|
|
69
|
+
path: string,
|
|
70
|
+
options: Partial<TRoute["options"]>,
|
|
71
|
+
controller: TDataProvider<TProvidedData> | null,
|
|
72
|
+
renderer: TFrontRenderer<TProvidedData>
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
// Route definition passed by the server
|
|
76
|
+
export type TSsrUnresolvedRoute = {
|
|
77
|
+
chunk: string,
|
|
78
|
+
} & ({
|
|
79
|
+
// Normal route
|
|
80
|
+
regex: string,
|
|
81
|
+
keys: TRoute["keys"]
|
|
82
|
+
} | {
|
|
83
|
+
// Error
|
|
84
|
+
code: number
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Route definition without having loaded the controller
|
|
88
|
+
type TUnresolvedRoute = TUnresolvedErrorRoute | TUnresolvedNormalRoute;
|
|
89
|
+
|
|
90
|
+
export type TUnresolvedErrorRoute = {
|
|
91
|
+
index: number,
|
|
92
|
+
chunk: string,
|
|
93
|
+
code: number,
|
|
94
|
+
load: TRouteLoader<TErrorRoute>,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type TUnresolvedNormalRoute = {
|
|
98
|
+
index: number,
|
|
99
|
+
chunk: string,
|
|
100
|
+
code: number,
|
|
101
|
+
load: TRouteLoader<TErrorRoute>,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type TRouteLoader<Route extends TRoute | TErrorRoute = TRoute | TErrorRoute> = () => Promise<TRouteModule<Route>>;
|
|
105
|
+
|
|
106
|
+
export type TFetchedRoute = Pick<TRoute, 'path' | 'options' | 'controller' | 'method'>
|
|
107
|
+
|
|
108
|
+
export type TRoutesLoaders = {
|
|
109
|
+
[chunkId: string]: () => Promise</* Preloaded via require() */TFetchedRoute | /* Loader via import() */TRouteLoader/* | undefined*/>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*----------------------------------
|
|
113
|
+
- SERVICE TYPES
|
|
114
|
+
----------------------------------*/
|
|
115
|
+
|
|
116
|
+
export type THookCallback<TRouter extends ClientRouter> = (request: ClientRequest<TRouter>) => void;
|
|
117
|
+
|
|
118
|
+
type THookName = 'location.change' | 'page.changed'
|
|
119
|
+
|
|
120
|
+
type Config = {
|
|
121
|
+
preload: string[], // List of globs
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*----------------------------------
|
|
125
|
+
- ROUTER
|
|
126
|
+
----------------------------------*/
|
|
127
|
+
export default class ClientRouter<
|
|
128
|
+
TApplication extends ClientApplication = ClientApplication
|
|
129
|
+
> extends Service<Config, ClientApplication> implements BaseRouter {
|
|
130
|
+
|
|
131
|
+
public ssrData = window["ssr"] as (TBasicSSrData | undefined);
|
|
132
|
+
public ssrRoutes = window["routes"] as TSsrUnresolvedRoute[];
|
|
133
|
+
|
|
134
|
+
public constructor(app: TApplication, config: Config) {
|
|
135
|
+
|
|
136
|
+
super(app, config);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public async start() {
|
|
140
|
+
|
|
141
|
+
const currentRoute = await this.registerRoutes();
|
|
142
|
+
|
|
143
|
+
this.initialRender(currentRoute);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public go( url: string ) {
|
|
147
|
+
history?.replace(url);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*----------------------------------
|
|
151
|
+
- REGISTRATION
|
|
152
|
+
----------------------------------*/
|
|
153
|
+
|
|
154
|
+
public routes: (TRoute | TUnresolvedNormalRoute)[] = [];
|
|
155
|
+
public errors: { [code: number]: TErrorRoute | TUnresolvedErrorRoute } = {};
|
|
156
|
+
|
|
157
|
+
public async registerRoutes() {
|
|
158
|
+
|
|
159
|
+
const loaders: TRoutesLoaders = { ...coreRoutes, ...appRoutes }
|
|
160
|
+
let currentRoute: TUnresolvedRoute | undefined;
|
|
161
|
+
debug && console.log(LogPrefix, `Indexing routes and finding the current route from ssr data:`, this.ssrData);
|
|
162
|
+
|
|
163
|
+
// Associe la liste des routes (obtenue via ssr) à leur loader
|
|
164
|
+
for (let routeIndex = 0; routeIndex < this.ssrRoutes.length; routeIndex++) {
|
|
165
|
+
|
|
166
|
+
const ssrRoute = this.ssrRoutes[routeIndex];
|
|
167
|
+
|
|
168
|
+
if (loaders[ssrRoute.chunk] === undefined) {
|
|
169
|
+
console.error("Chunk id not found for ssr route:", ssrRoute, "Searched in:", loaders);
|
|
170
|
+
throw new Error(`Loader not found for chunk id ${ssrRoute.chunk}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// TODO: Fix types
|
|
174
|
+
const loader = loaders[ssrRoute.chunk];
|
|
175
|
+
|
|
176
|
+
// Register the route
|
|
177
|
+
let route: TUnresolvedRoute;
|
|
178
|
+
if ('code' in ssrRoute)
|
|
179
|
+
route = this.errors[ssrRoute.code] = {
|
|
180
|
+
code: ssrRoute.code,
|
|
181
|
+
chunk: ssrRoute.chunk,
|
|
182
|
+
load: loader,
|
|
183
|
+
}
|
|
184
|
+
else
|
|
185
|
+
route = this.routes[routeIndex] = {
|
|
186
|
+
index: routeIndex,
|
|
187
|
+
chunk: ssrRoute.chunk,
|
|
188
|
+
regex: new RegExp(ssrRoute.regex),
|
|
189
|
+
keys: ssrRoute.keys,
|
|
190
|
+
load: loader,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
debug && console.log(LogPrefix, `${route.chunk}`, route);
|
|
194
|
+
|
|
195
|
+
// Detect if it's the current route
|
|
196
|
+
if (currentRoute === undefined) {
|
|
197
|
+
|
|
198
|
+
const isCurrentRoute = (
|
|
199
|
+
this.ssrData !== undefined
|
|
200
|
+
&&
|
|
201
|
+
route.chunk === this.ssrData.page.chunkId
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (isCurrentRoute) {
|
|
205
|
+
currentRoute = route;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return currentRoute;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public page(...args: TRegisterPageArgs): TRoute {
|
|
215
|
+
|
|
216
|
+
const { path, options, controller, renderer } = getRegisterPageArgs(...args);
|
|
217
|
+
|
|
218
|
+
// S'il s'agit d'une page, son id doit avoir été injecté via le plugin babel
|
|
219
|
+
const id = options["id"];
|
|
220
|
+
if (id === undefined)
|
|
221
|
+
throw new Error(`ID had not been injected into page options via the routes babel plugin for route ${path}.`);
|
|
222
|
+
|
|
223
|
+
const { regex, keys } = buildRegex(path);
|
|
224
|
+
|
|
225
|
+
const route: TRoute = {
|
|
226
|
+
method: 'GET',
|
|
227
|
+
path,
|
|
228
|
+
regex,
|
|
229
|
+
keys,
|
|
230
|
+
options: {
|
|
231
|
+
...defaultOptions,
|
|
232
|
+
...options
|
|
233
|
+
},
|
|
234
|
+
controller: (context: TClientOrServerContext) => new ClientPage(controller, renderer, context)
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
this.routes.push(route);
|
|
238
|
+
|
|
239
|
+
return route;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public error(code: number, options: TRoute["options"], renderer: TFrontRenderer<{}, { message: string }>) {
|
|
243
|
+
|
|
244
|
+
const route: TErrorRoute = {
|
|
245
|
+
code,
|
|
246
|
+
controller: (context: TClientOrServerContext) => new ClientPage(null, renderer, context),
|
|
247
|
+
options
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
this.errors[code] = route;
|
|
251
|
+
|
|
252
|
+
return route;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
/*----------------------------------
|
|
257
|
+
- RESOLUTION
|
|
258
|
+
----------------------------------*/
|
|
259
|
+
public async resolve(request: ClientRequest<this>): Promise<ClientPage | undefined | null> {
|
|
260
|
+
|
|
261
|
+
debug && console.log(LogPrefix, 'Resolving request', request.path, Object.keys(request.data));
|
|
262
|
+
this.runHook('location.change', request);
|
|
263
|
+
|
|
264
|
+
for (let iRoute = 0; iRoute < this.routes.length; iRoute++) {
|
|
265
|
+
|
|
266
|
+
let route = this.routes[iRoute];
|
|
267
|
+
if (!('regex' in route))
|
|
268
|
+
continue;
|
|
269
|
+
|
|
270
|
+
const match = route.regex.exec(request.path);
|
|
271
|
+
if (!match)
|
|
272
|
+
continue;
|
|
273
|
+
|
|
274
|
+
// URL data
|
|
275
|
+
for (let iKey = 0; iKey < route.keys.length; iKey++) {
|
|
276
|
+
const nomParam = route.keys[iKey];
|
|
277
|
+
if (typeof nomParam === 'string') // number = sans nom
|
|
278
|
+
request.data[nomParam] = match[iKey + 1]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Create response
|
|
282
|
+
debug && console.log(LogPrefix, 'Resolved request', request.path, '| Route:', route);
|
|
283
|
+
const page = await this.createResponse(route, request);
|
|
284
|
+
|
|
285
|
+
return page;
|
|
286
|
+
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async load(route: TUnresolvedNormalRoute): Promise<TRoute>;
|
|
293
|
+
private async load(route: TUnresolvedErrorRoute): Promise<TErrorRoute>;
|
|
294
|
+
private async load(route: TUnresolvedNormalRoute | TUnresolvedErrorRoute): Promise<TRoute | TErrorRoute> {
|
|
295
|
+
|
|
296
|
+
//throw new Error(`Failed to load route: ${route.chunk}`);
|
|
297
|
+
|
|
298
|
+
let fetched: TFetchedRoute;
|
|
299
|
+
if (typeof route.load === 'function') {
|
|
300
|
+
|
|
301
|
+
debug && console.log(`Fetching route ${route.chunk} ...`, route);
|
|
302
|
+
try {
|
|
303
|
+
|
|
304
|
+
const loaded = await route.load();
|
|
305
|
+
|
|
306
|
+
fetched = loaded.__register(this.app);
|
|
307
|
+
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.error(`Failed to fetch the route ${route.chunk}`, e);
|
|
310
|
+
this.app.handleError(new Error("Failed to load content. Please make sure you're connected to Internet."));
|
|
311
|
+
throw e;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
} else {
|
|
315
|
+
|
|
316
|
+
debug && console.log(`Route already fetched: ${route.chunk}`, route.load);
|
|
317
|
+
fetched = route.load;
|
|
318
|
+
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
debug && console.log(`Route fetched: ${route.chunk}`, fetched);
|
|
322
|
+
return {
|
|
323
|
+
...fetched,
|
|
324
|
+
regex: route.regex,
|
|
325
|
+
keys: route.keys
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
public set(data: TObjetDonnees) {
|
|
330
|
+
throw new Error(`router.set was not attached to the router component.`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private async initialRender(route: TUnresolvedRoute | undefined) {
|
|
334
|
+
|
|
335
|
+
debug && console.log(LogPrefix, `Initial render route`, route);
|
|
336
|
+
|
|
337
|
+
if (!location)
|
|
338
|
+
throw new Error(`Unable to retrieve current location.`);
|
|
339
|
+
|
|
340
|
+
if (!route)
|
|
341
|
+
throw new Error(`Unable to resolve route.`);
|
|
342
|
+
|
|
343
|
+
const request = new ClientRequest(location, this);
|
|
344
|
+
|
|
345
|
+
// Restituate SSR response
|
|
346
|
+
if (this.ssrData) {
|
|
347
|
+
|
|
348
|
+
console.log("SSR Response restitution ...");
|
|
349
|
+
|
|
350
|
+
request.user = this.ssrData.user || null;
|
|
351
|
+
|
|
352
|
+
request.data = this.ssrData.request.data;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const response = await this.createResponse(route, request)
|
|
356
|
+
|
|
357
|
+
ReactDOM.hydrate(<App context={response.context} />, document.body, () => {
|
|
358
|
+
|
|
359
|
+
console.log(`Render complete`);
|
|
360
|
+
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async createResponse(
|
|
365
|
+
route: TUnresolvedRoute | TRoute,
|
|
366
|
+
request: ClientRequest<this>,
|
|
367
|
+
additionnalData: {} = {}
|
|
368
|
+
): Promise<ClientPage> {
|
|
369
|
+
|
|
370
|
+
// Load if not done before
|
|
371
|
+
if ('load' in route)
|
|
372
|
+
route = this.routes[route.index] = await this.load(route);
|
|
373
|
+
|
|
374
|
+
// Run controller
|
|
375
|
+
// TODO: tell that ruController on the client side always returns pages
|
|
376
|
+
try {
|
|
377
|
+
|
|
378
|
+
const response = new ClientResponse<this, ClientPage>(request, route);
|
|
379
|
+
return await response.runController(additionnalData);
|
|
380
|
+
|
|
381
|
+
} catch (error) {
|
|
382
|
+
|
|
383
|
+
return await this.createErrorResponse(error, request);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async createErrorResponse(
|
|
388
|
+
e: any,
|
|
389
|
+
request: ClientRequest<this>,
|
|
390
|
+
additionnalData: {} = {}
|
|
391
|
+
): Promise<ClientPage> {
|
|
392
|
+
|
|
393
|
+
const code = 'http' in e ? e.http : 500;
|
|
394
|
+
console.log(`Loading error page ` + code);
|
|
395
|
+
let route = this.errors[code];
|
|
396
|
+
|
|
397
|
+
// Nor page configurated for this error
|
|
398
|
+
if (route === undefined) {
|
|
399
|
+
console.error(`Error page for http error code ${code} not found.`, this.errors, this.routes);
|
|
400
|
+
this.app.handleError(e, 404);
|
|
401
|
+
throw new Error(`Error page for http error code ${code} not found.`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Load if not done before
|
|
405
|
+
if ('load' in route)
|
|
406
|
+
route = this.errors[code] = await this.load(route);
|
|
407
|
+
|
|
408
|
+
const response = new ClientResponse<this, ClientPage>(request, route);
|
|
409
|
+
return await response.runController(additionnalData);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/*----------------------------------
|
|
413
|
+
- HOOKS
|
|
414
|
+
----------------------------------*/
|
|
415
|
+
private hooks: {
|
|
416
|
+
[hookname in THookName]?: (THookCallback<this> | null)[]
|
|
417
|
+
} = {}
|
|
418
|
+
|
|
419
|
+
public on(hookName: THookName, callback: THookCallback<this>) {
|
|
420
|
+
|
|
421
|
+
debug && console.info(LogPrefix, `Register hook ${hookName}`);
|
|
422
|
+
|
|
423
|
+
let cbIndex: number;
|
|
424
|
+
let callbacks = this.hooks[hookName];
|
|
425
|
+
if (!callbacks) {
|
|
426
|
+
cbIndex = 0;
|
|
427
|
+
callbacks = this.hooks[hookName] = [callback]
|
|
428
|
+
} else {
|
|
429
|
+
cbIndex = callbacks.length;
|
|
430
|
+
callbacks.push(callback);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Listener remover
|
|
434
|
+
return () => {
|
|
435
|
+
debug && console.info(LogPrefix, `De-register hook ${hookName} (index ${cbIndex})`);
|
|
436
|
+
delete (callbacks as THookCallback<this>[])[cbIndex];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
public runHook(hookName: THookName, request: ClientRequest<this>) {
|
|
442
|
+
const callbacks = this.hooks[hookName];
|
|
443
|
+
if (callbacks)
|
|
444
|
+
for (const callback of callbacks)
|
|
445
|
+
// callback can be null since we use delete to unregister
|
|
446
|
+
callback && callback(request);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import axios, { AxiosResponse, AxiosError, AxiosRequestConfig } from 'axios';
|
|
7
|
+
|
|
8
|
+
// Core
|
|
9
|
+
import type { TApiResponseData } from '@server/services/router';
|
|
10
|
+
import ApiClientService, {
|
|
11
|
+
TApiFetchOptions, TFetcherList, TFetcherArgs, TFetcher
|
|
12
|
+
} from '@common/router/request/api';
|
|
13
|
+
import { instancierViaCode, NetworkError } from '@common/errors';
|
|
14
|
+
import type ClientApplication from '@client/app';
|
|
15
|
+
|
|
16
|
+
// Specific
|
|
17
|
+
import type { default as Router, Request } from '..';
|
|
18
|
+
|
|
19
|
+
/*----------------------------------
|
|
20
|
+
- TYPES
|
|
21
|
+
----------------------------------*/
|
|
22
|
+
|
|
23
|
+
const debug = true;
|
|
24
|
+
|
|
25
|
+
export type Config = {
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*----------------------------------
|
|
30
|
+
- FUNCTION
|
|
31
|
+
----------------------------------*/
|
|
32
|
+
export default class ApiClient implements ApiClientService {
|
|
33
|
+
|
|
34
|
+
// APO Client needs to know the current request so we can monitor which api request is made from which page
|
|
35
|
+
public constructor(
|
|
36
|
+
public app: ClientApplication,
|
|
37
|
+
public request: Request<Router>
|
|
38
|
+
) {
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*----------------------------------
|
|
43
|
+
- HIGH LEVEL
|
|
44
|
+
----------------------------------*/
|
|
45
|
+
public get = <TData extends unknown = unknown>(path: string, data?: TObjetDonnees, opts?: TApiFetchOptions) =>
|
|
46
|
+
this.createFetcher<TData>('GET', path, data, opts);
|
|
47
|
+
|
|
48
|
+
public post = <TData extends unknown = unknown>(path: string, data?: TObjetDonnees, opts?: TApiFetchOptions) =>
|
|
49
|
+
this.createFetcher<TData>('POST', path, data, opts);
|
|
50
|
+
|
|
51
|
+
public put = <TData extends unknown = unknown>(path: string, data?: TObjetDonnees, opts?: TApiFetchOptions) =>
|
|
52
|
+
this.createFetcher<TData>('PUT', path, data, opts);
|
|
53
|
+
|
|
54
|
+
public delete = <TData extends unknown = unknown>(path: string, data?: TObjetDonnees, opts?: TApiFetchOptions) =>
|
|
55
|
+
this.createFetcher<TData>('DELETE', path, data, opts);
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
public set( newData: TObjetDonnees ) {
|
|
59
|
+
|
|
60
|
+
console.log("[api] Update page data", newData);
|
|
61
|
+
if (this.app.page)
|
|
62
|
+
this.app.page.setAllData(curData => ({ ...curData, ...newData }));
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public reload( ids?: string | string[], params?: TObjetDonnees ) {
|
|
67
|
+
|
|
68
|
+
if (this.app.page === undefined)
|
|
69
|
+
throw new Error("context.page is missing");
|
|
70
|
+
|
|
71
|
+
if (ids === undefined)
|
|
72
|
+
ids = Object.keys(this.app.page.fetchers);
|
|
73
|
+
else if (typeof ids === 'string')
|
|
74
|
+
ids = [ids];
|
|
75
|
+
|
|
76
|
+
console.log("[api] Reload data", ids, params, this.app.page.fetchers);
|
|
77
|
+
|
|
78
|
+
for (const id of ids) {
|
|
79
|
+
|
|
80
|
+
const fetcher = this.app.page.fetchers[id];
|
|
81
|
+
if (fetcher === undefined)
|
|
82
|
+
return console.error(`Unable to reload ${id}: Request not found in fetchers list.`);
|
|
83
|
+
|
|
84
|
+
if (params !== undefined)
|
|
85
|
+
fetcher.data = { ...(fetcher.data || {}), ...params };
|
|
86
|
+
|
|
87
|
+
console.log("[api][reload]", id, fetcher.method, fetcher.path, fetcher.data);
|
|
88
|
+
const indicator = this.toast.loading("Loading ...");
|
|
89
|
+
|
|
90
|
+
this.fetchAsync(fetcher.method, fetcher.path, fetcher.data).then((data) => {
|
|
91
|
+
|
|
92
|
+
this.set({ [id]: data });
|
|
93
|
+
|
|
94
|
+
}).finally(() => {
|
|
95
|
+
|
|
96
|
+
indicator.close(true);
|
|
97
|
+
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/*----------------------------------
|
|
103
|
+
- LOW LEVEL
|
|
104
|
+
----------------------------------*/
|
|
105
|
+
public createFetcher<TData extends unknown = unknown>(...args: TFetcherArgs): TFetcher<TData> {
|
|
106
|
+
const [method, path, data, options] = args;
|
|
107
|
+
return {
|
|
108
|
+
method, path, data, options,
|
|
109
|
+
// For async calls: api.post(...).then((data) => ...)
|
|
110
|
+
then: (callback: (data: any) => void) => this.fetchAsync<TData>(...args).then(callback),
|
|
111
|
+
catch: (callback: (data: any) => void) => this.fetchAsync(...args).catch(callback),
|
|
112
|
+
finally: (callback: () => void) => this.fetchAsync(...args).finally(callback),
|
|
113
|
+
run: () => this.fetchAsync(...args)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async fetchAsync<TData extends unknown = unknown>(...[
|
|
118
|
+
method, path, data, options
|
|
119
|
+
]: TFetcherArgs): Promise<TData> {
|
|
120
|
+
|
|
121
|
+
/*if (options?.captcha !== undefined)
|
|
122
|
+
await this.gui.captcha.check(options?.captcha);*/
|
|
123
|
+
|
|
124
|
+
return await this.fetch<TData>(method, path, data, options).catch((e) => {
|
|
125
|
+
this.app.handleError(e);
|
|
126
|
+
throw e;
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public async fetchSync(fetchers: TFetcherList): Promise<TObjetDonnees> {
|
|
131
|
+
|
|
132
|
+
const ids = Object.keys(fetchers);
|
|
133
|
+
if (ids.length === 1) {
|
|
134
|
+
const id = ids[0];
|
|
135
|
+
const fetcher = fetchers[id];
|
|
136
|
+
return {
|
|
137
|
+
[id]: await this.fetch( fetcher.method, fetcher.path, fetcher.data, undefined )
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fetchersArgs: {[id: string]: TFetcherArgs} = {};
|
|
142
|
+
for (const id in fetchers)
|
|
143
|
+
fetchersArgs[id] = [
|
|
144
|
+
fetchers[id].method,
|
|
145
|
+
fetchers[id].path,
|
|
146
|
+
fetchers[id].data,
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
return await this.fetch("POST", "/api", { fetchers: fetchersArgs }, undefined).then((res) => {
|
|
150
|
+
|
|
151
|
+
const data: TObjetDonnees = {};
|
|
152
|
+
for (const id in res)
|
|
153
|
+
data[id] = res[id];
|
|
154
|
+
|
|
155
|
+
return data;
|
|
156
|
+
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public configure = (...[method, path, data, options]: TFetcherArgs): AxiosRequestConfig => {
|
|
162
|
+
|
|
163
|
+
const { onProgress, captcha } = options || {};
|
|
164
|
+
|
|
165
|
+
debug && console.log(`[api] Sending request`, method, path, data);
|
|
166
|
+
|
|
167
|
+
const config: AxiosRequestConfig = {
|
|
168
|
+
|
|
169
|
+
url: path,
|
|
170
|
+
method: method,
|
|
171
|
+
headers: {
|
|
172
|
+
'Content-Type': "application/json",
|
|
173
|
+
'Accept': "application/json",
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
validateStatus: function (status: number) {
|
|
177
|
+
return status === 200;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
onUploadProgress: onProgress === undefined ? undefined : (e) => {
|
|
181
|
+
const percentCompleted = Math.round((e.loaded * 100) / e.total);
|
|
182
|
+
onProgress(percentCompleted);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (data) {
|
|
188
|
+
if (method === "GET")
|
|
189
|
+
config.params = data;
|
|
190
|
+
else {
|
|
191
|
+
config.data = data;
|
|
192
|
+
if (data instanceof FormData)
|
|
193
|
+
config.headers["Content-Type"] = 'multipart/form-data';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return config;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public fetch<TData = unknown>(...args: TFetcherArgs): Promise<TData> {
|
|
201
|
+
|
|
202
|
+
const config = this.configure(...args);
|
|
203
|
+
|
|
204
|
+
return axios.request(config)
|
|
205
|
+
.then((res: AxiosResponse<TApiResponseData>) => {
|
|
206
|
+
|
|
207
|
+
debug && console.log(`[api] Success:`, res);
|
|
208
|
+
return res.data as TData;
|
|
209
|
+
|
|
210
|
+
})
|
|
211
|
+
.catch((e: AxiosError) => {
|
|
212
|
+
|
|
213
|
+
if (e.response !== undefined) {
|
|
214
|
+
|
|
215
|
+
console.warn(`[api] Failure:`, e);
|
|
216
|
+
throw instancierViaCode(
|
|
217
|
+
e.response.status || 500,
|
|
218
|
+
e.response.data
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Erreur réseau: l'utilisateur n'ets probablement plus connecté à internet
|
|
222
|
+
} else {
|
|
223
|
+
const error = new NetworkError(e.message);
|
|
224
|
+
this.app.handleError(error);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
File without changes
|