5htp-core 0.6.1 → 0.6.2-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.
@@ -12,16 +12,12 @@ import Button, { Props as BtnProps } from '@client/components/Button';
12
12
  import { InputWrapper } from '../utils';
13
13
  import useContext from '@/client/context';
14
14
 
15
- // specific
16
- import FileToUpload from './FileToUpload';
17
-
18
15
  // Ressources
19
16
  import './index.less';
20
17
 
21
18
  /*----------------------------------
22
19
  - OUTILS
23
20
  ----------------------------------*/
24
- export { default as FileToUpload } from './FileToUpload';
25
21
 
26
22
  export const createImagePreview = (file: Blob) => new Promise((resolve, reject) => {
27
23
 
@@ -44,15 +40,6 @@ export const createImagePreview = (file: Blob) => new Promise((resolve, reject)
44
40
  reader.readAsDataURL(file);
45
41
  });
46
42
 
47
- // Instanciate FileToUpload from browser side File
48
- const normalizeFile = (file: File) => new FileToUpload({
49
- name: file.name,
50
- type: file.type,
51
- size: file.size,
52
- data: file,
53
- //original: file
54
- })
55
-
56
43
  /*----------------------------------
57
44
  - TYPES
58
45
  ----------------------------------*/
@@ -61,7 +48,7 @@ export type Props = {
61
48
 
62
49
  // Input
63
50
  title: string,
64
- value?: string | FileToUpload, // string = already registered
51
+ value?: string | File, // string = already registered
65
52
 
66
53
  // Display
67
54
  emptyText?: ComponentChild,
@@ -70,7 +57,7 @@ export type Props = {
70
57
  button?: boolean | BtnProps,
71
58
 
72
59
  // Actions
73
- onChange: (file: FileToUpload | undefined) => void
60
+ onChange: (file: File | undefined) => void
74
61
  remove?: () => Promise<void>,
75
62
  }
76
63
 
@@ -115,9 +102,7 @@ export default (props: Props) => {
115
102
  const selectedfile = fileSelectEvent.target.files[0] as File;
116
103
  if (selectedfile) {
117
104
 
118
- const fileToUpload = normalizeFile(selectedfile);
119
-
120
- onChange(fileToUpload);
105
+ onChange(selectedfile);
121
106
  }
122
107
  }
123
108
 
@@ -125,7 +110,7 @@ export default (props: Props) => {
125
110
 
126
111
  // Image = decode & display preview
127
112
  if (file !== undefined && typeof file === 'object' && file.type.startsWith('image/'))
128
- createImagePreview(file.data).then(setPreviewUrl);
113
+ createImagePreview(file).then(setPreviewUrl);
129
114
  else
130
115
  setPreviewUrl(undefined);
131
116
 
@@ -13,8 +13,8 @@ import {
13
13
 
14
14
  // Core libs
15
15
  import { InputBaseProps, useMantineInput } from './utils';
16
- import { default as Validator } from '../../common/validation/validator';
17
- import type { SchemaValidators } from '@common/validation/validators';
16
+ import { default as Validator } from '../../server/services/router/request/validation/validator';
17
+ import type { SchemaValidators } from '@server/services/router/request/validation/validators';
18
18
 
19
19
  /*----------------------------------
20
20
  - TYPES
@@ -151,8 +151,8 @@ export default ({ service: clientRouter, loaderComponent }: TProps) => {
151
151
  // But when we call setLayout, the style of the previous layout are still oaded and applied
152
152
  // Find a way to unload the previous layout / page resources before to load the new one
153
153
  console.log(LogPrefix, `Changing layout. Before:`, curLayout, 'New layout:', newLayout);
154
- window.location.replace( request ? request.url : window.location.href );
155
- return page; // Don't spread since it's an instance
154
+ /*window.location.replace( request ? request.url : window.location.href );
155
+ return page; // Don't spread since it's an instance*/
156
156
 
157
157
  context.app.setLayout(newLayout);
158
158
  }
@@ -15,7 +15,6 @@ import ApiClientService, {
15
15
  // Specific
16
16
  import type { default as Router, Request } from '..';
17
17
  import { toMultipart } from './multipart';
18
- import FileToUpload from '@client/components/File/FileToUpload';
19
18
 
20
19
  /*----------------------------------
21
20
  - TYPES
@@ -118,16 +117,36 @@ export default class ApiClient implements ApiClientService {
118
117
  // For async calls: api.post(...).then((data) => ...)
119
118
  then: (callback: (data: any) => void) => this.fetchAsync<TData>(...args)
120
119
  .then(callback)
121
- .catch( e => this.app.handleError(e)),
120
+ .catch( e => {
121
+ this.app.handleError(e);
122
+
123
+ // Don't run what is next
124
+ return {
125
+ then: () => {},
126
+ catch: () => {},
127
+ finally: (callback: () => void) => {
128
+ callback();
129
+ },
130
+ }
131
+ }),
122
132
 
123
133
  // Default error behavior only if not handled before by the app
124
- catch: (callback: (data: any) => void) => this.fetchAsync<TData>(...args)
134
+ catch: (callback: (data: any) => false | void) => this.fetchAsync<TData>(...args)
125
135
  .catch((e) => {
126
- try {
127
- callback(e);
128
- } catch (error) {
129
- this.app.handleError(e)
136
+
137
+ const shouldThrow = callback(e);
138
+ if (shouldThrow)
139
+ this.app.handleError(e);
140
+
141
+ // Don't run what is next
142
+ return {
143
+ then: () => {},
144
+ catch: () => {},
145
+ finally: (callback: () => void) => {
146
+ callback();
147
+ },
130
148
  }
149
+
131
150
  }),
132
151
 
133
152
  finally: (callback: () => void) => this.fetchAsync<TData>(...args)
@@ -206,7 +225,7 @@ export default class ApiClient implements ApiClientService {
206
225
 
207
226
  // If file included in data, need to use multipart
208
227
  // TODO: deep check
209
- const hasFile = Object.values(data).some((value) => value instanceof FileToUpload);
228
+ const hasFile = Object.values(data).some((value) => value instanceof File);
210
229
  if (hasFile) {
211
230
  // GET request = Can't send files
212
231
  if (method === "GET")
@@ -4,7 +4,6 @@
4
4
 
5
5
  // Core
6
6
  import { TPostData } from '@common/router/request/api';
7
- import { FileToUpload } from '@client/components/File';
8
7
 
9
8
  /*----------------------------------
10
9
  - TYPES
@@ -75,10 +74,6 @@ function convertRecursively(
75
74
  }
76
75
  }
77
76
 
78
- // Exract the file object from value
79
- if (typeof value === 'object' && value instanceof FileToUpload)
80
- value = value.data;
81
-
82
77
  if (isArray(value) || isJsonObject(value)) {
83
78
 
84
79
  convertRecursively(value, options, formData, propName);
@@ -43,6 +43,7 @@ export type ServerBug = {
43
43
 
44
44
  // Context
45
45
  hash: string,
46
+ isDuplicate: boolean,
46
47
  date: Date, // Timestamp
47
48
  channelType?: string,
48
49
  channelId?: string,
@@ -3,7 +3,7 @@
3
3
  ----------------------------------*/
4
4
 
5
5
  // Npm
6
- import zod from 'zod';
6
+ import type zod from 'zod';
7
7
 
8
8
  // types
9
9
  import type {
@@ -102,6 +102,7 @@ export type TRouteOptions = {
102
102
  refresh?: string,
103
103
  urls: string[]
104
104
  },
105
+ whenStatic?: boolean, // If true, the route is only executed even if the page is cached
105
106
  canonicalParams?: string[], // For SEO + unique ID for static cache
106
107
  layout?: false | string, // The nale of the layout
107
108
 
@@ -4,8 +4,6 @@
4
4
 
5
5
  import type { HttpMethod } from '@server/services/router';
6
6
 
7
- import type { FileToUpload } from '@client/components/File';
8
-
9
7
  /*----------------------------------
10
8
  - TYPES
11
9
  ----------------------------------*/
@@ -18,6 +16,8 @@ export type TFetcher<TData extends any = unknown> = {
18
16
 
19
17
  // For async calls: api.post(...).then((data) => ...)
20
18
  then: (callback: (data: TData) => void) => Promise<TData>,
19
+ catch: (callback: (data: any) => false | void) => Promise<TData>,
20
+ finally: (callback: () => void) => Promise<TData>,
21
21
  run: () => Promise<TData>,
22
22
 
23
23
  method: HttpMethod,
@@ -42,7 +42,7 @@ export type TApiFetchOptions = {
42
42
 
43
43
  export type TPostData = TPostDataWithFile
44
44
 
45
- export type TPostDataWithFile = { [key: string]: PrimitiveValue | FileToUpload }
45
+ export type TPostDataWithFile = { [key: string]: PrimitiveValue }
46
46
 
47
47
  export type TPostDataWithoutFile = { [key: string]: PrimitiveValue }
48
48
 
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.6.1",
4
+ "version": "0.6.2-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",
@@ -67,8 +67,8 @@
67
67
  "object-sizeof": "^1.6.3",
68
68
  "path-to-regexp": "^6.2.0",
69
69
  "picomatch": "^2.3.1",
70
- "preact": "^10.22.1",
71
- "preact-render-to-string": "^6.5.5",
70
+ "preact": "^10.27.1",
71
+ "preact-render-to-string": "^6.6.1",
72
72
  "prettier": "^3.3.3",
73
73
  "react-scrollbars-custom": "^4.0.27",
74
74
  "react-slider": "^2.0.1",
@@ -88,7 +88,7 @@
88
88
  "yargs-parser": "^21.1.1",
89
89
  "youch": "^3.3.3",
90
90
  "youch-terminal": "^2.2.3",
91
- "zod": "^3.24.2"
91
+ "zod": "^4.1.5"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@types/cookie": "^0.4.1",
@@ -5,6 +5,8 @@
5
5
  // Node
6
6
  import { serialize } from 'v8';
7
7
  import { formatWithOptions } from 'util';
8
+ import md5 from 'md5';
9
+ import dayjs from 'dayjs';
8
10
  import Youch from 'youch';
9
11
  import forTerminal from 'youch-terminal';
10
12
 
@@ -147,6 +149,12 @@ export default class Console {
147
149
  public logger!: Logger<ILogObj>;
148
150
  // Buffers
149
151
  public logs: TJsonLog[] = [];
152
+ private reported: {
153
+ [hash: string]: {
154
+ times: number,
155
+ last: Date,
156
+ }
157
+ } = {};
150
158
 
151
159
  /*----------------------------------
152
160
  - LIFECYCLE
@@ -282,35 +290,12 @@ export default class Console {
282
290
  // We don't prevent duplicates because we want to receive all variants of the same error
283
291
  public async createBugReport( error: TCatchedError, request?: ServerRequest ) {
284
292
 
285
- /*const youchRes = new Youch(error, {});
286
- const jsonResponse = await youchRes.toJSON()
287
- console.log( forTerminal(jsonResponse, {
288
- // Defaults to false
289
- displayShortPath: false,
290
-
291
- // Defaults to single whitspace
292
- prefix: ' ',
293
-
294
- // Defaults to false
295
- hideErrorTitle: false,
296
-
297
- // Defaults to false
298
- hideMessage: false,
299
-
300
- // Defaults to false
301
- displayMainFrameOnly: false,
302
-
303
- // Defaults to 3
304
- framesMaxLimit: 3,
305
- }) );*/
306
-
307
293
  const application = this.container.application;
308
294
  if (application === undefined)
309
295
  return console.error(LogPrefix, "Can't send bug report because the application is not instanciated");
310
296
 
311
297
  // Get context
312
298
  const now = new Date();
313
- const hash = uuid();
314
299
  const { channelType, channelId } = this.getChannel();
315
300
 
316
301
  // On envoi l'email avant l'insertion dans bla bdd
@@ -353,10 +338,35 @@ export default class Console {
353
338
  : undefined
354
339
  }
355
340
 
341
+ // Genertae unique error hash
342
+ const hash = md5( stacktraces.join('\n') );
343
+
344
+ // Don't send the same error twice in a row (avoid email spamming)
345
+ const lastReport = this.reported[hash];
346
+ let isDuplicate = false;
347
+ if (lastReport === undefined) {
348
+
349
+ this.reported[hash] = {
350
+ times: 0,
351
+ last: new Date()
352
+ }
353
+
354
+ // If error older than 1 day
355
+ } else if (dayjs(now).diff( dayjs(lastReport.last), 'day' ) > 1) {
356
+
357
+ lastReport.times++;
358
+ lastReport.last = now;
359
+
360
+ } else {
361
+
362
+ isDuplicate = true;
363
+ }
364
+
356
365
  const bugReport: ServerBug = {
357
366
 
358
367
  // Context
359
368
  hash: hash,
369
+ isDuplicate,
360
370
  date: now,
361
371
  channelType,
362
372
  channelId,
@@ -208,6 +208,8 @@ export abstract class Application<
208
208
  ? route.schema.parse( context.request.data )
209
209
  : {};
210
210
 
211
+ console.log('-----data', data);
212
+
211
213
  // Run controller
212
214
  return origController.bind( service )(
213
215
  data,
@@ -9,7 +9,7 @@ import type { TServiceMetas } from './container';
9
9
  import type { TControllerDefinition, TRoute } from '../../services/router';
10
10
  import { Anomaly } from "@common/errors";
11
11
 
12
- export { default as schema } from 'zod';
12
+ export { schema } from '../../services/router/request/validation/zod';
13
13
 
14
14
  /*----------------------------------
15
15
  - TYPES: OPTIONS
@@ -171,7 +171,7 @@ export default abstract class Email<TConfig extends Config>
171
171
 
172
172
  });
173
173
 
174
- console.info(LogPrefix, `Sending ${emailsToSend.length} emails via transporter`, emailsToSend[0].subject);
174
+ console.info(LogPrefix, `Sending ${emailsToSend.length} emails:`, emailsToSend[0].subject);
175
175
 
176
176
  // Pas d'envoi d'email quand local
177
177
  if (this.app.env.name === 'local' && this.config.simulateWhenLocal === true) {
@@ -6,7 +6,6 @@
6
6
  import mime from 'mime-types';
7
7
 
8
8
  // Core
9
- import FileToUpload from '@client/components/File/FileToUpload';
10
9
  import { InputError } from '@common/errors';
11
10
 
12
11
  /*----------------------------------
@@ -82,7 +81,11 @@ export const traiterMultipart = (...canaux: any[]) => {
82
81
  &&
83
82
  donnee.data instanceof Buffer
84
83
  ){
85
- donnee = normalizeFile(donnee);
84
+ donnee = new File(donnee.data, donnee.name, {
85
+ type: donnee.mimetype,
86
+ lastModified: Date.now(),
87
+ //size: donnee.size,
88
+ });
86
89
  }
87
90
 
88
91
  brancheA[ cle ] = donnee;
@@ -91,24 +94,4 @@ export const traiterMultipart = (...canaux: any[]) => {
91
94
  }
92
95
 
93
96
  return sortie;
94
- }
95
-
96
- const normalizeFile = (file: UploadedFile) => {
97
-
98
- const ext = mime.extension(file.mimetype);
99
-
100
- if (ext === false)
101
- throw new InputError(`We couldn't determine the type of the CV file you sent. Please encure it's not corrupted and try again.`);
102
-
103
- return new FileToUpload({
104
-
105
- name: file.name,
106
- type: file.mimetype,
107
- size: file.size,
108
-
109
- data: file.data,
110
-
111
- md5: file.md5,
112
- ext: ext
113
- })
114
97
  }
@@ -221,38 +221,37 @@ export default class ServerRouter
221
221
  - ACTIONS
222
222
  ----------------------------------*/
223
223
 
224
- private async renderStatic(
225
- path: string,
224
+ public async renderStatic(
225
+ url: string,
226
226
  options: TRouteOptions["static"],
227
227
  rendered?: any
228
228
  ) {
229
229
 
230
230
  // Wildcard: tell that the newly rendered pages should be cached
231
- if (path === '*' || !path)
231
+ if (url === '*' || !url)
232
232
  return;
233
233
 
234
234
  if (!rendered) {
235
235
 
236
- const fullUrl = this.url(path, {}, true);
237
- console.log('[router] renderStatic', fullUrl);
238
-
236
+ const fullUrl = this.url(url, {}, true);
239
237
  const response = await got( fullUrl, {
240
238
  method: 'GET',
241
239
  headers: {
242
- 'Accept': 'text/html'
240
+ 'Accept': 'text/html',
241
+ 'bypasscache': '1'
243
242
  },
244
243
  throwHttpErrors: false,
245
244
  });
246
245
 
247
246
  if (response.statusCode !== 200) {
248
- console.error('renderStatic', response.statusCode, response.body);
247
+ console.error("[router] renderStatic: page returned code", response.statusCode, fullUrl);
249
248
  return;
250
249
  }
251
250
 
252
251
  rendered = response.body;
253
252
  }
254
253
 
255
- this.cache[path] = {
254
+ this.cache[url] = {
256
255
  rendered: rendered,
257
256
  options: options,
258
257
  expire: typeof options === 'object'
@@ -266,11 +265,11 @@ export default class ServerRouter
266
265
 
267
266
  console.log('[router] refreshStaticPages');
268
267
 
269
- for (const pageId in this.cache) {
270
- const page = this.cache[pageId];
271
- if (page.path && page.expire && page.expire < Date.now()) {
268
+ for (const pageUrl in this.cache) {
269
+ const page = this.cache[pageUrl];
270
+ if (page.expire && page.expire < Date.now()) {
272
271
 
273
- this.renderStatic(page.path, page.options);
272
+ this.renderStatic(pageUrl, page.options);
274
273
 
275
274
  }
276
275
  }
@@ -499,15 +498,12 @@ export default class ServerRouter
499
498
  "no-store, no-cache, must-revalidate, proxy-revalidate"
500
499
  );
501
500
 
502
- // Static pages
503
- if (this.cache[req.path]) {
504
- console.log('[router] Get static page from cache', req.path);
505
- res.send( this.cache[req.path].rendered );
506
- return;
507
- }
508
-
509
501
  // Create request
510
502
  let requestId = uuid();
503
+ const cachedPage = req.headers['bypasscache']
504
+ ? undefined
505
+ : this.cache[req.path];
506
+
511
507
  const request = new ServerRequest(
512
508
  requestId,
513
509
 
@@ -533,13 +529,25 @@ export default class ServerRouter
533
529
  return await this.resolveApiBatch(request.data.fetchers, request);
534
530
 
535
531
  } else {
536
- response = await this.resolve(request);
532
+ response = await this.resolve(
533
+ request,
534
+ // If cached page, we only run routes with priority >= 10
535
+ cachedPage ? true : false
536
+ );
537
537
  }
538
538
  } catch (e) {
539
539
  response = await this.handleError(e, request);
540
540
  }
541
541
 
542
542
  if (!res.headersSent) {
543
+
544
+ // Static pages
545
+ if (cachedPage) {
546
+ console.log('[router] Get static page from cache', req.path);
547
+ res.send( cachedPage.rendered );
548
+ return;
549
+ }
550
+
543
551
  // Status
544
552
  res.status(response.statusCode);
545
553
  // Headers
@@ -572,7 +580,10 @@ export default class ServerRouter
572
580
  return contextServices;
573
581
  }
574
582
 
575
- public resolve = (request: ServerRequest<this>) => new Promise<ServerResponse<this>>((resolve, reject) => {
583
+ public resolve = (
584
+ request: ServerRequest<this>,
585
+ isStatic?: boolean
586
+ ) => new Promise<ServerResponse<this>>((resolve, reject) => {
576
587
 
577
588
  // Create request context so we can access request context across all the request-triggered libs
578
589
  context.run({
@@ -613,6 +624,9 @@ export default class ServerRouter
613
624
  // Classic routes
614
625
  for (route of this.routes) {
615
626
 
627
+ if (isStatic && !route.options.whenStatic)
628
+ continue;
629
+
616
630
  // Match Method
617
631
  if (request.method !== route.method && route.method !== '*')
618
632
  continue;
@@ -710,11 +724,11 @@ export default class ServerRouter
710
724
  request.res.json(responseData);
711
725
  }
712
726
 
713
- private async handleError( e: Error |CoreError | ZodError, request: ServerRequest<ServerRouter> ) {
727
+ private async handleError( e: Error | CoreError | ZodError, request: ServerRequest<ServerRouter> ) {
714
728
 
715
729
  if (e instanceof ZodError)
716
730
  e = new InputError(
717
- e.errors.map(e => e.path.join('.') + ': ' + e.message).join(', ')
731
+ e.issues.map((e) => e.path.join('.') + ': ' + e.message).join(', ')
718
732
  );
719
733
 
720
734
  const code = 'http' in e ? e.http : 500;
@@ -10,7 +10,6 @@ import Bowser from "bowser";
10
10
 
11
11
  // Core
12
12
  import BaseRequest from '@common/router/request';
13
- import type FileToUpload from '@client/components/File/FileToUpload';
14
13
 
15
14
  // Specific
16
15
  import type {
@@ -40,7 +39,7 @@ const localeFilter = (input: any) => {
40
39
  return lang.toUpperCase();
41
40
  }
42
41
 
43
- export type UploadedFile = With<FileToUpload, 'md5'|'ext'>
42
+ export type UploadedFile = File
44
43
 
45
44
  /*----------------------------------
46
45
  - CONTEXTE
@@ -13,7 +13,6 @@ import normalizeUrl, { Options as NormalizeUrlOptions } from 'normalize-url';
13
13
 
14
14
  // Core
15
15
  import { InputError } from '@common/errors';
16
- import FileToUpload from '@client/components/File/FileToUpload';
17
16
 
18
17
  // Speciific
19
18
  import Schema, { TSchemaFields } from './schema'
@@ -23,7 +22,7 @@ import Validator, { TValidatorOptions, EXCLUDE_VALUE, TValidatorDefinition } fro
23
22
  - TYPES
24
23
  ----------------------------------*/
25
24
 
26
- export type TFileValidator = TValidatorOptions<FileToUpload> & {
25
+ export type TFileValidator = TValidatorOptions<File> & {
27
26
  type?: string[], // Raccourci, ou liste de mimetype
28
27
  taille?: number,
29
28
  disk?: string, // Disk to upload files to
@@ -447,14 +446,14 @@ export class SchemaValidators {
447
446
  ----------------------------------*/
448
447
  public file = ({ type, taille, ...opts }: TFileValidator & {
449
448
 
450
- } = {}) => new Validator<FileToUpload>('file', (val, options, path) => {
449
+ } = {}) => new Validator<File>('file', (val, options, path) => {
451
450
 
452
451
  // Chaine = url ancien fichier = exclusion de la valeur pour conserver l'ancien fichier
453
452
  // NOTE: Si la valeur est présente mais undefined, alors on supprimera le fichier
454
453
  if (typeof val === 'string')
455
454
  return EXCLUDE_VALUE;
456
455
 
457
- if (!(val instanceof FileToUpload))
456
+ if (!(val instanceof File))
458
457
  throw new InputError(`Must be a File (${typeof val} received)`);
459
458
 
460
459
  // MIME
@@ -0,0 +1,97 @@
1
+ import { InputError } from '@common/errors';
2
+ import zod from 'zod';
3
+
4
+ export type TRichTextValidatorOptions = {
5
+ attachements?: boolean
6
+ }
7
+
8
+ // Recursive function to validate each node
9
+ function validateLexicalNode(node: any, opts: TRichTextValidatorOptions ) {
10
+
11
+ // Each node should be an object with a `type` property
12
+ if (typeof node !== 'object' || !node.type || typeof node.type !== 'string')
13
+ throw new InputError("Invalid rich text value (3).");
14
+
15
+ // Validate text nodes
16
+ if (node.type === 'text') {
17
+
18
+ if (typeof node.text !== 'string')
19
+ throw new InputError("Invalid rich text value (4).");
20
+
21
+ // Validate paragraph, heading, or other structural nodes that may contain children
22
+ } else if (['paragraph', 'heading', 'list', 'listitem'].includes(node.type)) {
23
+
24
+ if (!Array.isArray(node.children) || !node.children.every(children => validateLexicalNode(children, opts))) {
25
+ throw new InputError("Invalid rich text value (5).");
26
+ }
27
+
28
+ // Files upload
29
+ } else if (node.type === 'image') {
30
+
31
+ // Check if allowed
32
+ /*if (opts.attachements === undefined)
33
+ throw new InputError("Image attachments not allowed in this rich text field.");*/
34
+
35
+ // TODO: check mime
36
+
37
+
38
+ // Upload file
39
+
40
+
41
+ }
42
+
43
+ return true;
44
+ }
45
+
46
+ export const schema = {
47
+ ...zod,
48
+
49
+ file: (builder: (file: zod.ZodType<File>) => any) => schema.custom(val => {
50
+
51
+ // Chaine = url ancien fichier = exclusion de la valeur pour conserver l'ancien fichier
52
+ // NOTE: Si la valeur est présente mais undefined, alors on supprimera le fichier
53
+ if (typeof val === 'string')
54
+ return true;
55
+
56
+ // Default file validation
57
+ const fileInstance = zod.file();
58
+
59
+ return builder(fileInstance).parse(val);
60
+ }),
61
+
62
+ richText: (opts: TRichTextValidatorOptions = {}) => schema.custom(val => {
63
+
64
+ if (typeof val !== 'string') {
65
+ console.error("Invalid rich text format.", val);
66
+ return false;
67
+ }
68
+
69
+ // We get a stringified json as input since the editor workds with JSON string
70
+ try {
71
+ val = JSON.parse(val);
72
+ } catch (error) {
73
+ console.error("Failed to parse rich text json:", error, val);
74
+ return false;//throw new InputError("Invalid rich text format.");
75
+ }
76
+
77
+ // Check that the root exists and has a valid type
78
+ if (!val || typeof val !== 'object' || typeof val.root !== 'object' || val.root.type !== 'root') {
79
+ console.error("Invalid rich text value (1).", val);
80
+ return false;//throw new InputError("Invalid rich text value (1).");
81
+ }
82
+
83
+ // Check if root has children array
84
+ if (!Array.isArray(val.root.children)) {
85
+ console.error("Invalid rich text value (2).", val);
86
+ return false;
87
+ }
88
+
89
+ // Validate each child node in root
90
+ for (const child of val.root.children) {
91
+ if (!validateLexicalNode(child, opts))
92
+ return false;
93
+ }
94
+
95
+ return true;
96
+ })
97
+ }
@@ -6,10 +6,8 @@
6
6
  import type { Application } from '@server/app';
7
7
 
8
8
  // Specific
9
- import { SchemaValidators, TFileValidator } from '@common/validation/validators';
10
- import Validator, { TValidatorOptions } from '@common/validation/validator';
11
-
12
- import type FileToUpload from '@client/components/File/FileToUpload';
9
+ import { SchemaValidators, TFileValidator } from '@server/services/router/request/validation/validators';
10
+ import Validator, { TValidatorOptions } from '@server/services/router/request/validation/validator';
13
11
 
14
12
  /*----------------------------------
15
13
  - TYPES
@@ -7,7 +7,7 @@ import {
7
7
  default as Router, RequestService, Request as ServerRequest
8
8
  } from '@server/services/router';
9
9
 
10
- import Schema, { TSchemaFields, TValidatedData } from '@common/validation/schema';
10
+ import Schema, { TSchemaFields, TValidatedData } from '@server/services/router/request/validation/schema';
11
11
 
12
12
  // Specific
13
13
  import ServerSchemaValidator from '.';
package/types/icons.d.ts CHANGED
@@ -1 +1 @@
1
- export type TIcones = "solid/spinner-third"|"long-arrow-right"|"times-circle"|"brands/whatsapp"|"times"|"search"|"user"|"rocket"|"globe"|"bullhorn"|"briefcase"|"chart-line"|"handshake"|"ellipsis-h"|"brands/google"|"brands/reddit-alien"|"brands/linkedin-in"|"brands/github"|"robot"|"comments"|"user-friends"|"angle-down"|"mouse-pointer"|"thumbs-up"|"dollar-sign"|"info-circle"|"check-circle"|"exclamation-circle"|"chart-bar"|"power-off"|"heart"|"lock"|"eye"|"credit-card"|"at"|"brands/linkedin"|"key"|"exclamation"|"solid/download"|"bars"|"font"|"tag"|"compress"|"bolt"|"puzzle-piece"|"planet-ringed"|"database"|"solid/fire"|"usd-circle"|"lightbulb"|"solid/dollar-sign"|"download"|"code"|"solid/clock"|"seedling"|"palette"|"car"|"plane"|"university"|"hard-hat"|"graduation-cap"|"cogs"|"film"|"leaf"|"tshirt"|"utensils"|"map-marked-alt"|"dumbbell"|"stethoscope"|"concierge-bell"|"book"|"shield-alt"|"gavel"|"industry"|"square-root-alt"|"newspaper"|"pills"|"medal"|"capsules"|"balance-scale"|"home"|"praying-hands"|"shopping-cart"|"flask"|"futbol"|"microchip"|"satellite-dish"|"shipping-fast"|"passport"|"tools"|"user-circle"|"plus-circle"|"brands/twitter"|"brands/facebook"|"comment-alt"|"paper-plane"|"check"|"angle-left"|"angle-right"|"trash"|"arrow-left"|"arrow-right"|"meh-rolling-eyes"|"bold"|"italic"|"underline"|"link"|"strikethrough"|"subscript"|"superscript"|"empty-set"|"horizontal-rule"|"page-break"|"image"|"table"|"poll"|"columns"|"sticky-note"|"caret-right"|"file"|"unlink"|"pen"|"plus"|"list-ul"|"check-square"|"h1"|"h2"|"h3"|"h4"|"list-ol"|"paragraph"|"quote-left"|"align-left"|"align-center"|"align-right"|"align-justify"|"indent"|"outdent"
1
+ export type TIcones = "solid/spinner-third"|"long-arrow-right"|"times-circle"|"brands/whatsapp"|"times"|"search"|"angle-down"|"user"|"rocket"|"globe"|"bullhorn"|"briefcase"|"chart-line"|"handshake"|"ellipsis-h"|"brands/google"|"brands/reddit-alien"|"brands/linkedin-in"|"brands/github"|"robot"|"comments"|"user-friends"|"mouse-pointer"|"thumbs-up"|"dollar-sign"|"info-circle"|"check-circle"|"exclamation-circle"|"home"|"user-circle"|"newspaper"|"plus-circle"|"brands/linkedin"|"brands/twitter"|"brands/facebook"|"comment-alt"|"heart"|"lock"|"eye"|"credit-card"|"at"|"key"|"chart-bar"|"power-off"|"bars"|"font"|"tag"|"compress"|"bolt"|"puzzle-piece"|"planet-ringed"|"seedling"|"palette"|"car"|"plane"|"university"|"hard-hat"|"graduation-cap"|"cogs"|"film"|"leaf"|"tshirt"|"utensils"|"map-marked-alt"|"dumbbell"|"stethoscope"|"concierge-bell"|"book"|"shield-alt"|"gavel"|"industry"|"square-root-alt"|"pills"|"medal"|"capsules"|"balance-scale"|"praying-hands"|"shopping-cart"|"flask"|"futbol"|"microchip"|"satellite-dish"|"shipping-fast"|"passport"|"tools"|"exclamation"|"solid/download"|"database"|"solid/fire"|"usd-circle"|"lightbulb"|"solid/dollar-sign"|"download"|"code"|"solid/clock"|"paper-plane"|"check"|"long-arrow-left"|"angle-left"|"angle-right"|"trash"|"arrow-left"|"arrow-right"|"meh-rolling-eyes"|"unlink"|"pen"|"link"|"file"|"bold"|"italic"|"underline"|"strikethrough"|"subscript"|"superscript"|"plus"|"empty-set"|"horizontal-rule"|"page-break"|"image"|"table"|"poll"|"columns"|"sticky-note"|"caret-right"|"align-left"|"align-center"|"align-right"|"align-justify"|"indent"|"outdent"|"list-ul"|"check-square"|"h1"|"h2"|"h3"|"h4"|"list-ol"|"paragraph"|"quote-left"
@@ -1,34 +0,0 @@
1
- // Normalize file between browser and nodejs side
2
- export default class FileToUpload {
3
-
4
- public name: string;
5
- public size: number;
6
- public type: string;
7
-
8
- public data: File;
9
-
10
- // Retrieved on backend only
11
- public md5?: string;
12
- public ext?: string;
13
-
14
- public constructor(opts: {
15
- name: string,
16
- size: number,
17
- type: string,
18
-
19
- data: File,
20
-
21
- md5?: string,
22
- ext?: string,
23
- }) {
24
-
25
- this.name = opts.name;
26
- this.size = opts.size;
27
- this.type = opts.type;
28
-
29
- this.data = opts.data;
30
-
31
- this.md5 = opts.md5;
32
- this.ext = opts.ext;
33
- }
34
- }
@@ -1,275 +0,0 @@
1
- /*----------------------------------
2
- - DEPENDANCES
3
- ----------------------------------*/
4
-
5
- // npm
6
- import React from 'react';
7
-
8
- // Core
9
- import { InputErrorSchema } from '@common/errors';
10
- import type { Schema } from '@common/validation';
11
- import type { TValidationResult, TValidateOptions } from '@common/validation/schema';
12
- import useContext from '@/client/context';
13
-
14
- // Exports
15
- export type { TValidationResult, TSchemaData } from '@common/validation/schema';
16
-
17
- /*----------------------------------
18
- - TYPES
19
- ----------------------------------*/
20
- export type TFormOptions<TFormData extends {}> = {
21
- data?: Partial<TFormData>,
22
- submit?: (data: TFormData) => Promise<void>,
23
- autoValidateOnly?: (keyof TFormData)[],
24
- autoSave?: {
25
- id: string
26
- }
27
- }
28
-
29
- export type FieldsAttrs<TFormData extends {}> = {
30
- [fieldName in keyof TFormData]: {}
31
- }
32
-
33
- export type Form<TFormData extends {} = {}> = {
34
-
35
- // Data
36
- fields: FieldsAttrs<TFormData>,
37
- data: TFormData,
38
- options: TFormOptions<TFormData>,
39
- backup?: Partial<TFormData>,
40
-
41
- // Actions
42
- setBackup: (backup: Partial<TFormData>) => void,
43
- validate: (data: Partial<TFormData>, validateAll?: boolean) => TValidationResult<{}>,
44
- set: (data: Partial<TFormData>, merge?: boolean) => void,
45
- submit: (additionnalData?: Partial<TFormData>) => Promise<any>,
46
-
47
- } & FormState
48
-
49
- type FormState<TFormData extends {} = {}> = {
50
- isLoading: boolean,
51
- hasChanged: boolean,
52
- errorsCount: number,
53
- errors: { [fieldName: string]: string[] },
54
- backup?: Partial<TFormData>,
55
- }
56
-
57
- /*----------------------------------
58
- - HOOK
59
- ----------------------------------*/
60
- export default function useForm<TFormData extends {}>(
61
- schema: Schema<TFormData>,
62
- options: TFormOptions<TFormData> = {}
63
- ): [ Form<TFormData>, FieldsAttrs<TFormData> ] {
64
-
65
- const context = useContext();
66
-
67
- /*----------------------------------
68
- - INIT
69
- ----------------------------------*/
70
-
71
- const [state, setState] = React.useState<FormState<TFormData>>({
72
- hasChanged: false,//options.data !== undefined,
73
- isLoading: false,
74
- errorsCount: 0,
75
- errors: {},
76
- backup: undefined
77
- });
78
-
79
- const initialData: Partial<TFormData> = options.data || {};
80
-
81
- // States
82
- const fields = React.useRef<FieldsAttrs<TFormData> | null>(null);
83
- const [data, setData] = React.useState< Partial<TFormData> >(initialData);
84
-
85
- // When typed data changes
86
- React.useEffect(() => {
87
-
88
- // Validate
89
- validate(data, { ignoreMissing: true });
90
-
91
- // Autosave
92
- if (options.autoSave !== undefined && state.hasChanged) {
93
- saveLocally(data, options.autoSave.id);
94
- }
95
-
96
- }, [data]);
97
-
98
- // On start
99
- React.useEffect(() => {
100
-
101
- // Restore backup
102
- if (options.autoSave !== undefined && !state.hasChanged) {
103
-
104
- const autosaved = localStorage.getItem('form.' + options.autoSave.id);
105
- if (autosaved !== null) {
106
- try {
107
- console.log('[form] Parse autosaved from json:', autosaved);
108
- setState(c => ({
109
- ...c,
110
- backup: JSON.parse(autosaved)
111
- }));
112
- } catch (error) {
113
- console.error('[form] Failed to decode autosaved data from json:', autosaved);
114
- }
115
- }
116
- }
117
-
118
- }, []);
119
-
120
- /*----------------------------------
121
- - ACTIONS
122
- ----------------------------------*/
123
- const validate = (allData: Partial<TFormData> = data, opts: TValidateOptions<TFormData> = {}) => {
124
-
125
- const validated = schema.validateWithDetails(allData, allData, {}, {
126
- // Ignore the fields where the vlaue has not been changed
127
- // if the validation was triggered via onChange
128
- ignoreMissing: false,
129
- // The list of fields we should only validate
130
- only: options.autoValidateOnly,
131
- // Custom options
132
- ...opts
133
- });
134
-
135
- // Update errors
136
- if (validated.errorsCount !== state.errorsCount) {
137
- rebuildFieldsAttrs({
138
- errorsCount: validated.errorsCount,
139
- errors: validated.erreurs,
140
- });
141
- }
142
-
143
- return validated;
144
- }
145
-
146
- const submit = (additionnalData: Partial<TFormData> = {}) => {
147
-
148
- const allData = { ...data, ...additionnalData }
149
-
150
- // Validation
151
- const validated = validate(allData);
152
- if (validated.errorsCount !== 0) {
153
- throw new InputErrorSchema(validated.erreurs);
154
- }
155
-
156
- const afterSubmit = (responseData?: any) => {
157
-
158
- // Reset autosaved data
159
- if (options.autoSave)
160
- localStorage.removeItem('form.' + options.autoSave.id);
161
-
162
- // Update state
163
- setState( current => ({
164
- ...current,
165
- hasChanged: false
166
- }));
167
-
168
- return responseData;
169
- }
170
-
171
- // Callback
172
- if (options.submit)
173
- return options.submit(allData as TFormData).then(afterSubmit);
174
- else {
175
- afterSubmit();
176
- return undefined;
177
- }
178
- }
179
-
180
- const rebuildFieldsAttrs = (newState: Partial<FormState> = {}) => {
181
- // Force rebuilding the fields definition on the next state change
182
- fields.current = null;
183
- // Force state change
184
- setState(old => ({
185
- ...old,
186
- ...newState
187
- }));
188
- }
189
-
190
- const saveLocally = (data: Partial<TFormData>, id: string) => {
191
- console.log('[form] Autosave data for form:', id, ':', data);
192
- localStorage.setItem('form.' + id, JSON.stringify(data));
193
- }
194
-
195
- // Rebuild the fields attrs when the schema changes
196
- if (fields.current === null || Object.keys(schema).join(',') !== Object.keys(fields.current).join(',')) {
197
- fields.current = {} as FieldsAttrs<TFormData>
198
- for (const fieldName in schema.fields) {
199
-
200
- const validator = schema.getFieldValidator(fieldName);
201
-
202
- fields.current[fieldName] = {
203
-
204
- // Value control
205
- value: data[fieldName],
206
- onChange: (val) => {
207
- setData(old => {
208
- return {
209
- ...old,
210
- [fieldName]: typeof val === 'function'
211
- ? val(old[fieldName])
212
- : val
213
- }
214
- })
215
-
216
- setState(current => ({
217
- ...current,
218
- hasChanged: true
219
- }));
220
- },
221
-
222
- // Submit on press enter
223
- onKeyDown: e => {
224
- if (e.key === 'Enter' || (e.keyCode || e.which) === 13) {
225
- submit({ [fieldName]: e.target.value } as Partial<TFormData>);
226
- }
227
- },
228
-
229
- // Error
230
- errors: state.errors[fieldName],
231
-
232
- // Component attributes
233
- ...validator.componentAttributes
234
- }
235
- }
236
- }
237
-
238
- /*----------------------------------
239
- - EXPOSE
240
- ----------------------------------*/
241
-
242
- const form: Form<TFormData> = {
243
-
244
- fields: fields.current,
245
- data,
246
- set: (data, merge = true) => {
247
-
248
- setState( current => ({
249
- ...current,
250
- hasChanged: true
251
- }));
252
-
253
- setData( merge
254
- ? c => ({ ...c, ...data })
255
- : data
256
- );
257
- },
258
-
259
- validate,
260
- submit,
261
- options,
262
-
263
- setBackup: (backup: Partial<TFormData>) => {
264
-
265
- setState(c => ({ ...c, backup }));
266
-
267
- if (options.autoSave)
268
- localStorage.setItem('form.' + options.autoSave.id, JSON.stringify(backup));
269
- },
270
-
271
- ...state
272
- }
273
-
274
- return [form, fields.current]
275
- }