@beseif-solutions/prow-core 1.0.0 → 1.0.2

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.
@@ -1,15 +1,11 @@
1
- import Joi, { DateSchema, NumberSchema, StringSchema } from "joi";
2
- import _ from "lodash";
3
- import moment from "moment-timezone";
4
- import context from "./context";
5
- import { where, Where } from "./where";
1
+ import { Where } from "./where";
6
2
 
7
3
  export type Condition = {
8
4
  condition: Where,
9
5
  overrides: Partial<Field>,
10
6
  };
11
7
 
12
- type Common = {
8
+ export type CommonField = {
13
9
  key: string,
14
10
  altered_by?: Condition[],
15
11
  disabled?: boolean,
@@ -29,9 +25,9 @@ type Common = {
29
25
  & Record<`experimental_${string}`, any>
30
26
  ;
31
27
 
32
- export type Field<Extend = Record<never, never>> = Common & InputField<Extend>;
28
+ export type Field<Extend = Record<never, never>> = CommonField & InputField<Extend>;
33
29
 
34
- type InputField<Extend = Record<never, never>> = (
30
+ export type InputField<Extend = Record<never, never>> = (
35
31
  | AnyInputField<Extend>
36
32
  | StringInputField<Extend>
37
33
  | NumberInputField<Extend>
@@ -43,9 +39,9 @@ type InputField<Extend = Record<never, never>> = (
43
39
  | FileInputField<Extend>
44
40
  ) & Extend;
45
41
 
46
- type Any = string | number | boolean | Any[] | { [key: string]: Any };
42
+ export type Any = string | number | boolean | Any[] | { [key: string]: Any };
47
43
 
48
- type AnyInputField<Extend = Record<never, never>> = {
44
+ export type AnyInputField<Extend = Record<never, never>> = {
49
45
  type: `any`,
50
46
  default?: Any | Any[],
51
47
  allow_nullish?: boolean, // allow null values
@@ -54,7 +50,7 @@ type AnyInputField<Extend = Record<never, never>> = {
54
50
  | {}
55
51
  );
56
52
 
57
- type StringInputField<Extend = Record<never, never>> = {
53
+ export type StringInputField<Extend = Record<never, never>> = {
58
54
  type: `string`,
59
55
  default?: string | string[],
60
56
  length?: number,
@@ -70,7 +66,7 @@ type StringInputField<Extend = Record<never, never>> = {
70
66
  | {}
71
67
  );
72
68
 
73
- type NumberInputField<Extend = Record<never, never>> = {
69
+ export type NumberInputField<Extend = Record<never, never>> = {
74
70
  type: `number`,
75
71
  default?: number | number[],
76
72
  min?: number,
@@ -83,7 +79,7 @@ type NumberInputField<Extend = Record<never, never>> = {
83
79
  | {}
84
80
  );
85
81
 
86
- type BooleanInputField<Extend = Record<never, never>> = {
82
+ export type BooleanInputField<Extend = Record<never, never>> = {
87
83
  type: `boolean`,
88
84
  default?: boolean | boolean[],
89
85
  } & (
@@ -93,7 +89,7 @@ type BooleanInputField<Extend = Record<never, never>> = {
93
89
  | {}
94
90
  );
95
91
 
96
- type DateTimeInputField<Extend = Record<never, never>> = {
92
+ export type DateTimeInputField<Extend = Record<never, never>> = {
97
93
  type: `datetime`,
98
94
  timezone?: string,
99
95
  default?: string | number | (string | number)[],
@@ -104,7 +100,7 @@ type DateTimeInputField<Extend = Record<never, never>> = {
104
100
  | {}
105
101
  );
106
102
 
107
- type DateInputField<Extend = Record<never, never>> = {
103
+ export type DateInputField<Extend = Record<never, never>> = {
108
104
  type: `date`,
109
105
  default?: string | string[],
110
106
  min?: string,
@@ -114,7 +110,7 @@ type DateInputField<Extend = Record<never, never>> = {
114
110
  | {}
115
111
  );
116
112
 
117
- type TimeInputField<Extend = Record<never, never>> = {
113
+ export type TimeInputField<Extend = Record<never, never>> = {
118
114
  type: `time`,
119
115
  timezone?: string,
120
116
  default?: string | string[],
@@ -123,7 +119,7 @@ type TimeInputField<Extend = Record<never, never>> = {
123
119
  | {}
124
120
  );
125
121
 
126
- type DictInputField<Extend = Record<never, never>> = {
122
+ export type DictInputField<Extend = Record<never, never>> = {
127
123
  type: `dict`,
128
124
  default?: Record<string, any> | Record<string, any>[],
129
125
  items?: Field<Extend>[],
@@ -136,7 +132,7 @@ export type File = {
136
132
  content: string, // base64 encoded content
137
133
  };
138
134
 
139
- type FileInputField<Extend = Record<never, never>> = {
135
+ export type FileInputField<Extend = Record<never, never>> = {
140
136
  type: `file`,
141
137
  default?: File | File[],
142
138
  accept?: string | string[], // mime type
@@ -146,7 +142,7 @@ type FileInputField<Extend = Record<never, never>> = {
146
142
  );
147
143
 
148
144
  /* select configuration */
149
- type SelectField<Values = string | number | boolean | File, Extend = Record<never, never>> = {
145
+ export type SelectField<Values = string | number | boolean | File, Extend = Record<never, never>> = {
150
146
  format: `select`,
151
147
  // el type string representa el nombre de una función del repositorio que devuelve SelectFieldChoice[]
152
148
  choices: SelectFieldChoice<Values, Extend>[] | string,
@@ -160,658 +156,11 @@ export type SelectFieldChoice<Values = string | number | boolean | File, Extend
160
156
  } & Extend;
161
157
 
162
158
  /* custom display configuration */
163
- type CustomDisplayTextField = {
159
+ export type CustomDisplayTextField = {
164
160
  format: `textarea` | `rich-text` | `markdown`,
165
161
  };
166
162
 
167
- type IDETextField = {
163
+ export type IDETextField = {
168
164
  format: `code`,
169
165
  language?: string, // language for code highlighting
170
- };
171
-
172
- type ValidationExtended = {
173
- original: any,
174
- value: any,
175
- } & ({
176
- valid: true,
177
- } | {
178
- valid: false,
179
- errors: {
180
- path: string,
181
- message: string,
182
- type: string,
183
- }[],
184
- });
185
-
186
- const conditionSchema = () =>
187
- Joi.object({
188
- condition: Joi.object().required(),
189
- overrides: Joi.object().required(),
190
- });
191
-
192
- const commonFieldSchema = () =>
193
- Joi.object({
194
- key: Joi.string().required(),
195
- altered_by: Joi.array().items(conditionSchema()),
196
- disabled: Joi.bool(),
197
- required: Joi.bool(),
198
- as_password: Joi.bool(),
199
- show: Joi.bool(),
200
- t: Joi.string(),
201
- as_array: Joi.bool(),
202
- array_min: Joi.when(`as_array`, {
203
- is: true,
204
- then: Joi.number(),
205
- otherwise: Joi.forbidden(),
206
- }),
207
- array_max: Joi.when(`as_array`, {
208
- is: true,
209
- then: Joi.number(),
210
- otherwise: Joi.forbidden(),
211
- }),
212
- array_length: Joi.when(`as_array`, {
213
- is: true,
214
- then: Joi.number(),
215
- otherwise: Joi.forbidden(),
216
- }),
217
- }).concat(Joi.object() // experimental properties for custom front features
218
- .pattern(Joi.string().regex(/^experimental_/), Joi.any()));
219
-
220
- const types = [`any`, `string`, `number`, `boolean`, `date`, `datetime`, `time`, `dict`, `file`];
221
-
222
- const inputFieldBaseSchema = () =>
223
- Joi.object({
224
- type: Joi.string().required()
225
- .valid(...types),
226
- // switch by type properties
227
- })
228
- .concat(commonFieldSchema())
229
- .unknown();
230
-
231
- const choiceSchema = (type: Joi.Schema) =>
232
- Joi.object({
233
- key: Joi.string().required(),
234
- value: type.required(),
235
- group: Joi.string(),
236
- }).unknown();
237
-
238
- const choicesSchema = (type: Joi.Schema) =>
239
- Joi.alternatives(
240
- Joi.string(),
241
- Joi.array().min(1)
242
- .items(choiceSchema(type))
243
- );
244
-
245
- const defaultSchema = (type: Joi.Schema) =>
246
- Joi.when(`as_array`, {
247
- is: true,
248
- then: Joi.array().items(type),
249
- otherwise: type,
250
- });
251
-
252
- const anySchema = (nullish = true) => {
253
- // eslint-disable-next-line @typescript-eslint/no-magic-numbers
254
- const randId = Math.random().toString(36).substring(2, 15);
255
- return Joi.alternatives(
256
- Joi.string(),
257
- Joi.bool(),
258
- Joi.number(),
259
- Joi.array().items(Joi.link(`#${randId}`)),
260
- Joi.object().pattern(Joi.string(), Joi.link(`#${randId}`)),
261
- ...nullish ? [Joi.valid(null, ``)] : [],
262
- )
263
- .id(randId);
264
- };
265
-
266
- const anyInputFieldSchema = () =>
267
- Joi.object({
268
- default: defaultSchema(anySchema()),
269
- allow_nullish: Joi.bool().default(true),
270
- })
271
- .concat(Joi.object({
272
- format: Joi.string().valid(`select`),
273
- choices: Joi.when(`format`, {
274
- is: `select`,
275
- then: choicesSchema(anySchema()).required(),
276
- otherwise: Joi.forbidden(),
277
- }),
278
- }));
279
-
280
- const stringInputFieldSchema = () =>
281
- Joi.object({
282
- default: defaultSchema(Joi.string()),
283
- length: Joi.number(),
284
- min: Joi.number(),
285
- max: Joi.number(),
286
- regexp: Joi.string(),
287
- pattern: Joi.string().valid(`email`, `uri`),
288
- allow_empty: Joi.bool().default(false),
289
- })
290
- .concat(
291
- Joi.object({
292
- format: Joi.string().valid(`select`, `textarea`, `rich-text`, `markdown`, `code`),
293
- choices: Joi.when(`format`, {
294
- is: `select`,
295
- then: choicesSchema(Joi.string()).required(),
296
- otherwise: Joi.forbidden(),
297
- }),
298
- language: Joi.when(`format`, {
299
- is: `code`,
300
- then: Joi.string(),
301
- otherwise: Joi.forbidden(),
302
- }),
303
- })
304
- );
305
-
306
- const numberInputFieldSchema = () =>
307
- Joi.object({
308
- default: defaultSchema(Joi.number()),
309
- min: Joi.number(),
310
- max: Joi.number(),
311
- float: Joi.bool(),
312
- decimals: Joi.when(`float`, {
313
- is: true,
314
- then: Joi.number(),
315
- otherwise: Joi.forbidden(),
316
- }),
317
- })
318
- .concat(Joi.object({
319
- format: Joi.string().valid(`select`),
320
- choices: Joi.when(`format`, {
321
- is: `select`,
322
- then: choicesSchema(Joi.number()).required(),
323
- otherwise: Joi.forbidden(),
324
- }),
325
- }));
326
-
327
- const booleanInputFieldSchema = () =>
328
- Joi.object({
329
- default: defaultSchema(Joi.bool()),
330
- })
331
- .concat(Joi.object({
332
- format: Joi.string().valid(`select`, `checkbox`, `switch`),
333
- choices: Joi.when(`format`, {
334
- is: `select`,
335
- then: choicesSchema(Joi.bool()).required(),
336
- otherwise: Joi.forbidden(),
337
- }),
338
- }));
339
-
340
- const dateInputFieldSchema = () =>
341
- Joi.object({
342
- default: defaultSchema(Joi.alternatives(Joi.string(), Joi.number())),
343
- min: Joi.alternatives(Joi.string(), Joi.number()),
344
- max: Joi.alternatives(Joi.string(), Joi.number()),
345
- })
346
- .concat(Joi.object({
347
- format: Joi.string().valid(`select`),
348
- choices: Joi.when(`format`, {
349
- is: `select`,
350
- then: choicesSchema(Joi.alternatives(Joi.string(), Joi.number())).required(),
351
- otherwise: Joi.forbidden(),
352
- }),
353
- }));
354
-
355
- const timeInputFieldSchema = () =>
356
- Joi.object({
357
- default: defaultSchema(Joi.alternatives(Joi.string(), Joi.number())),
358
- })
359
- .concat(Joi.object({
360
- format: Joi.string().valid(`select`),
361
- choices: Joi.when(`format`, {
362
- is: `select`,
363
- then: choicesSchema(Joi.alternatives(Joi.string(), Joi.number())).required(),
364
- otherwise: Joi.forbidden(),
365
- }),
366
- }));
367
-
368
- const dictInputFieldSchema = () =>
369
- Joi.object({
370
- default: defaultSchema(Joi.object()),
371
- items: Joi.array().items(Joi.link(`#field`)).min(1),
372
- unknown: Joi.when(`items`, {
373
- is: Joi.exist(),
374
- then: Joi.bool(),
375
- otherwise: Joi.forbidden(),
376
- }),
377
- });
378
-
379
- const fileSchema = () => Joi.object({
380
- name: Joi.string().required(),
381
- type: Joi.string().required(), // mime type
382
- content: Joi.string().required(), // base64 encoded content
383
- });
384
-
385
- // ÑAPA: translate file to structured dict
386
- const fileReplacer = (field: Common & FileInputField): Field => ({
387
- key: field.key,
388
- type: `dict`,
389
- unknown: true,
390
- items: [
391
- {
392
- key: `name`,
393
- type: `string`,
394
- required: true,
395
- },
396
- {
397
- key: `type`,
398
- type: `string`,
399
- required: true,
400
- },
401
- {
402
- key: `content`,
403
- type: `string`,
404
- required: true,
405
- },
406
- ],
407
- default: field.default,
408
- ..._.pick(field, [`altered_by`, `disabled`, `required`, `as_password`, `show`, `as_array`, `array_min`, `array_max`, `array_length`]),
409
- ...Object.assign({}, ...Object.entries(field).map(([k, v]) => k.startsWith(`experimental_`) ? { [k]: v } : {})),
410
- }) as Field;
411
-
412
- const fileInputFieldSchema = () =>
413
- Joi.object({
414
- default: defaultSchema(fileSchema()),
415
- accept: Joi.alternatives(
416
- Joi.string(),
417
- Joi.array().items(Joi.string())
418
- ).optional(),
419
- })
420
- .concat(
421
- Joi.object({
422
- format: Joi.string().valid(`select`),
423
- choices: Joi.when(`format`, {
424
- is: `select`,
425
- then: choicesSchema(fileSchema()).required(),
426
- otherwise: Joi.forbidden(),
427
- }),
428
- })
429
- );
430
-
431
- export const fieldSchema = () =>
432
- inputFieldBaseSchema()
433
- .id(`field`)
434
- .when(Joi.object({ type: Joi.valid(`any`) }).unknown(), {
435
- then: inputFieldBaseSchema()
436
- .concat(anyInputFieldSchema()),
437
- })
438
- .when(Joi.object({ type: Joi.valid(`string`) }).unknown(), {
439
- then: inputFieldBaseSchema()
440
- .concat(stringInputFieldSchema()),
441
- })
442
- .when(Joi.object({ type: Joi.valid(`number`) }).unknown(), {
443
- then: inputFieldBaseSchema()
444
- .concat(numberInputFieldSchema()),
445
- })
446
- .when(Joi.object({ type: Joi.valid(`boolean`) }).unknown(), {
447
- then: inputFieldBaseSchema()
448
- .concat(booleanInputFieldSchema()),
449
- })
450
- .when(Joi.object({ type: Joi.valid(`date`, `datetime`) }).unknown(), {
451
- then: inputFieldBaseSchema()
452
- .concat(dateInputFieldSchema()),
453
- })
454
- .when(Joi.object({ type: Joi.valid(`time`) }).unknown(), {
455
- then: inputFieldBaseSchema()
456
- .concat(timeInputFieldSchema()),
457
- })
458
- .when(Joi.object({ type: Joi.valid(`dict`) }).unknown(), {
459
- then: inputFieldBaseSchema()
460
- .concat(dictInputFieldSchema()),
461
- })
462
- .when(Joi.object({ type: Joi.valid(`file`) }).unknown(), {
463
- then: inputFieldBaseSchema()
464
- .concat(fileInputFieldSchema()),
465
- })
466
- // invalid fallback
467
- .when(Joi.object({ type: Joi.invalid(...types) }).unknown(), {
468
- then: Joi.forbidden(),
469
- });
470
-
471
- export const fieldsSchema = () =>
472
- Joi.array().items(fieldSchema());
473
-
474
- const getSimpleFieldSchema = (field: Field) => {
475
- if (field.as_array) { throw new Error(`Cannot use as_array with simple field schema generator`); }
476
-
477
- let typeSchema: Joi.Schema;
478
- switch (field.type) {
479
- case `any`:
480
- typeSchema = anySchema(field.allow_nullish);
481
- break;
482
- case `boolean`:
483
- typeSchema = Joi.bool();
484
- break;
485
- case `string`:
486
- typeSchema = Joi.string();
487
- if (field.length) { typeSchema = (typeSchema as StringSchema).length(field.length); }
488
- if (field.min > 0) {
489
- typeSchema = (typeSchema as StringSchema).min(field.min);
490
- } else if (field.allow_empty) {
491
- typeSchema = typeSchema.allow(``);
492
- }
493
- if (field.max) { typeSchema = (typeSchema as StringSchema).max(field.max); }
494
- if (field.regexp) { typeSchema = (typeSchema as StringSchema).regex(new RegExp(field.regexp)); }
495
- if (field.pattern === `email`) { typeSchema = (typeSchema as StringSchema).email(); }
496
- if (field.pattern === `uri`) { typeSchema = (typeSchema as StringSchema).uri(); }
497
-
498
- break;
499
- case `date`:
500
- case `datetime`:
501
- case `time`:
502
- if (field.type === `time`) {
503
- typeSchema = Joi.string();
504
- } else {
505
- typeSchema = Joi.date().strict(false);
506
- if (field.min) { typeSchema = (typeSchema as DateSchema).min(field.min); }
507
- if (field.max) { typeSchema = (typeSchema as DateSchema).max(field.max); }
508
- }
509
-
510
- typeSchema = typeSchema.external(async (value, helpers) => {
511
- if (!helpers.original && !field.required) { return helpers.original; }
512
-
513
- const format = field.type === `date` ? `YYYY-MM-DD` :
514
- field.type === `time` ? `HH:mmZ` :
515
- `YYYY-MM-DDTHH:mm:ssZ`;
516
- const d = (field.type === `datetime` && typeof helpers.original === `number`) ?
517
- moment.unix(helpers.original).tz(`UTC`)
518
- : moment.tz(helpers.original, format, `UTC`);
519
- if (!d.isValid()) { return helpers.message({ external: `invalid format, expected ${format}` }); }
520
-
521
- const timezone = (field.type === `date` ? `UTC` : field.timezone) || `UTC`;
522
- return d.tz(timezone).format(format);
523
- });
524
-
525
- break;
526
- case `number`:
527
- typeSchema = Joi.number();
528
- if (field.min) { typeSchema = (typeSchema as NumberSchema).min(field.min); }
529
- if (field.max) { typeSchema = (typeSchema as NumberSchema).max(field.max); }
530
- // don't check precision if float is not set
531
- if (typeof field.float === `boolean`) {
532
- if (!field.float) {
533
- typeSchema = (typeSchema as NumberSchema).precision(0);
534
- } else if (field.decimals) {
535
- typeSchema = (typeSchema as NumberSchema).precision(field.decimals);
536
- }
537
- }
538
-
539
- break;
540
- case `dict`:
541
- if (field.items) { throw new Error(`No simple field`); }
542
- typeSchema = Joi.object().unknown(true);
543
- break;
544
- case `file`:
545
- typeSchema = fileSchema();
546
- break;
547
- }
548
-
549
- if (`format` in field && field.format === `select`) {
550
- if (!field.unknown) {
551
- if (_.isArray(field.choices)) {
552
- typeSchema = typeSchema.valid(...field.choices.map((c: SelectFieldChoice) => c.value));
553
- } else {
554
- // async select values are not checked
555
- }
556
- }
557
- }
558
-
559
- return getGeneralSchema(field, typeSchema);
560
- };
561
-
562
- const getGeneralArraySchema = (field: Field, schema: Joi.ArraySchema) => {
563
- if (!field.as_array) { throw new Error(`as_array required for array schema generator`); }
564
-
565
- if (field.array_length) { schema = schema.length(field.array_length); }
566
- if (field.array_min) { schema = schema.min(field.array_min); }
567
- if (field.array_max) { schema = schema.max(field.array_max); }
568
-
569
- return getGeneralSchema(field, schema);
570
- };
571
-
572
- const getItemsArrayFieldSchema = (field: Field, alternatives: Joi.Schema[]) => {
573
- if (!field.as_array) { throw new Error(`as_array required for array schema generator`); }
574
-
575
- const schema = Joi.array().items(...alternatives);
576
- return getGeneralArraySchema(field, schema);
577
- };
578
-
579
- const getOrderedArrayFieldSchema = (field: Field, items: Joi.Schema[]) => {
580
- if (!field.as_array) { throw new Error(`as_array required for array schema generator`); }
581
-
582
- const schema = Joi.array().ordered(...items);
583
- return getGeneralArraySchema(field, schema);
584
- };
585
-
586
- const getObjectFieldSchema = (field: Field, items: { key: string, schema: Joi.Schema }[]) => {
587
- if (field.type !== `dict`) { throw new Error(`type must be dict for object schema generator`); }
588
- if (!field.items) { throw new Error(`items required for object schema generator`); }
589
-
590
- let schema = Joi.object();
591
- for (const item of items) {
592
- schema = schema.concat(Joi.object({
593
- [item.key]: item.schema,
594
- }));
595
- }
596
-
597
- if (field.unknown) { schema = schema.unknown(field.unknown); }
598
-
599
- return getGeneralSchema(field, schema);
600
- };
601
-
602
- const getGeneralSchema = (field: Field, schema: Joi.Schema): Joi.Schema => {
603
- if (field.required) { schema = schema.required(); }
604
- if (field.disabled) { schema = schema.forbidden(); }
605
- return schema;
606
- };
607
-
608
- const convertPath = (parts: (string | number)[]): string =>
609
- parts.map((p, i) => typeof p === `string` ? (i === 0 ? p : `.${p}`) : `[${p}]`).join(``);
610
-
611
- const secure = async (field: Field, validation: ValidationExtended): Promise<ValidationExtended> =>
612
- field.as_password ? _.omit(validation, [`value`, `original`]) as any : validation;
613
-
614
- export type ValidationResult<Extended = Record<string, never>> = {
615
- valid: boolean,
616
- fields: Field<Extended & ValidationExtended>[],
617
- };
618
-
619
- const relative_condition = (condition: Where, relative: string): typeof condition => {
620
- try {
621
- const newCondition = _.cloneDeep(condition);
622
- for (const key of Object.keys(newCondition)) {
623
- if (key.startsWith(`@relative`)) {
624
- const newKey = key.replace(`@relative`, relative ? relative : ``);
625
- newCondition[newKey] = newCondition[key];
626
- delete newCondition[key];
627
- }
628
- }
629
-
630
- return newCondition;
631
- } catch (e) { throw e; }
632
- };
633
-
634
- export const alter = (field: Field, relative: string, data: Record<string, any>) => {
635
- const cloned = _.cloneDeep(field);
636
- for (const alteration of cloned.altered_by) {
637
- const condition = relative_condition(alteration.condition, relative);
638
-
639
- const matches = where(data, [condition]);
640
- if (matches) { _.assign(cloned, alteration.overrides); }
641
- }
642
- return cloned;
643
- };
644
-
645
- const recursive_schema = async (field: Field, data: Record<string, any>, options: { alter: boolean, relative?: string, array?: boolean, context: Record<string, any> }): Promise<{ schema: Joi.Schema, field: Field }> => {
646
- try {
647
- let newField: Field = _.cloneDeep(field);
648
-
649
- // alterations over already resolved data (previous fields)
650
- if (newField.altered_by?.length > 0) {
651
- newField = alter(newField, options.relative, data);
652
-
653
- // remove altered_by only if it's not an array -> array alterations must be kept for front-end rendering
654
- if (options.alter) { delete newField.altered_by; }
655
-
656
- if (typeof newField.show === `boolean` && !newField.show) { return; }
657
- }
658
-
659
- const alteredField: Field = _.cloneDeep(newField);
660
-
661
- // ÑAPA: if the field is a file, replace it
662
- if (newField.type === `file`) { newField = fileReplacer(newField); }
663
-
664
- const relativeKey = options.relative ?
665
- options.relative.endsWith(`]`) ?
666
- options.array ? options.relative : `${options.relative}.${newField.key}`
667
- : `${options.relative}.${newField.key}`
668
- : newField.key;
669
-
670
- const value = _.get(data, relativeKey, newField.default);
671
-
672
- // resolve value with context (even if it's not valid - omit undefined)
673
- const resolved = context.resolve(newField, value, options.context);
674
- if (typeof resolved !== `undefined`) { _.set(data, relativeKey, resolved); }
675
-
676
- // get schema for field with data
677
- let calculatedSchema: Joi.Schema;
678
- if (newField.as_array) {
679
- const noArrayField = _.omit(newField, [`as_array`, `array_min`, `array_max`, `array_length`]) as Field;
680
-
681
- if (resolved) {
682
- const itemSchemas: Joi.Schema[] = [];
683
- if (_.isArray(resolved)) {
684
- for (let i = 0; i < (resolved || []).length; i++) {
685
- const { schema: itemSchema } = await recursive_schema(noArrayField, data, { alter: false, relative: `${relativeKey}[${i}]`, array: true, context: options.context });
686
- if (itemSchema) { itemSchemas.push(itemSchema); }
687
- }
688
- }
689
-
690
- calculatedSchema = getOrderedArrayFieldSchema(newField, itemSchemas);
691
- } else {
692
- let genericItemSchema: Joi.Schema;
693
- if (noArrayField.type === `dict` && noArrayField.items) {
694
- const items: { key: string, schema: Joi.Schema, field: Field }[] = [];
695
-
696
- for (const item of noArrayField.items) {
697
- const recursive = await recursive_schema(item, data, { alter: options.alter && true, relative: `${relativeKey}[0]`, context: options.context });
698
- if (recursive) { items.push({ key: item.key, schema: recursive.schema, field: recursive.field }); }
699
- }
700
-
701
- genericItemSchema = getObjectFieldSchema(noArrayField, items);
702
- noArrayField.items = items.map((i) => i.field);
703
- } else {
704
- genericItemSchema = getSimpleFieldSchema(noArrayField);
705
- }
706
-
707
- calculatedSchema = getItemsArrayFieldSchema(newField, [genericItemSchema]);
708
- }
709
- } else if (newField.type === `dict` && newField.items) {
710
- const items: { key: string, schema: Joi.Schema, field: Field }[] = [];
711
-
712
- for (const item of newField.items) {
713
- const recursive = await recursive_schema(item, data, { alter: options.alter && true, relative: relativeKey, context: options.context });
714
- if (recursive) { items.push({ key: item.key, schema: recursive.schema, field: recursive.field }); }
715
- }
716
-
717
- calculatedSchema = getObjectFieldSchema(newField, items);
718
- newField.items = items.map((i) => i.field);
719
- } else {
720
- calculatedSchema = getSimpleFieldSchema(newField);
721
- }
722
-
723
- if (!calculatedSchema) { throw new Error(`Could not resolve schema for field ${newField.key}`); }
724
-
725
- // if disabled schema validation is done but the value is not set
726
- if (newField.disabled) { _.unset(data, relativeKey); }
727
-
728
- return { schema: calculatedSchema, field: field.type === `file` ? alteredField : newField };
729
- } catch (e) { throw e; }
730
- };
731
-
732
- const validate_internal = async (fields: Field[], data: Record<string, any>, options: { secure: boolean, break: boolean, context: Record<string, any> }): Promise<ValidationResult> => {
733
- try {
734
- const response: ValidationResult<{}> = { valid: true, fields: [] };
735
-
736
- for (const field of fields) {
737
-
738
- const value = _.get(data, field.key, field.default);
739
-
740
- const cloned = _.cloneDeep(data);
741
-
742
- const recursive = await recursive_schema(field, cloned, { alter: true, context: options.context });
743
- if (!recursive) { continue; }
744
-
745
- const resolved = _.get(cloned, field.key, field.default);
746
-
747
- let validation: any;
748
- let error: any;
749
- try {
750
- validation = await recursive.schema.validateAsync(resolved, { abortEarly: false });
751
- } catch (e) { error = e; }
752
-
753
- let extended: ValidationExtended;
754
- if (error) {
755
- extended = {
756
- valid: false,
757
- errors: error.details ? error.details.map((d) => ({
758
- type: d.type,
759
- path: convertPath([field.key, ...d.path]),
760
- message: d.message,
761
- })) : [
762
- {
763
- type: `unknown`,
764
- path: field.key,
765
- message: error.message,
766
- },
767
- ],
768
- original: value,
769
- value: null,
770
- };
771
- } else {
772
- _.set(data, field.key, validation);
773
- extended = {
774
- valid: true,
775
- original: value,
776
- value: validation,
777
- };
778
- }
779
-
780
- response.valid &&= error ? false : true;
781
- response.fields.push({
782
- ...recursive.field as any,
783
- ...options.secure ? await secure(field, extended) : extended,
784
- });
785
-
786
- }
787
-
788
- return response;
789
- } catch (e) { throw e; }
790
- };
791
-
792
- export const validate = async <I>(fields: Field<I>[], data: Record<string, any>, options: { secure?: boolean, break?: boolean, context?: Record<string, any> } = {}): Promise<ValidationResult<I>> => {
793
- try {
794
- const { error } = fieldsSchema().validate(fields);
795
- if (error) { throw new Error(`Invalid fields definition: ${error.message}`); }
796
- const defaultOptions = _.assign({ secure: true, break: false, context: {} }, options);
797
- const response = await validate_internal(fields, data, defaultOptions);
798
- return response as any;
799
- } catch (e) { throw e; }
800
- };
801
-
802
- const extract_recursive = async (validation: Field<ValidationExtended>[], options: { original?: boolean }, path = ``): Promise<Record<string, any>> => {
803
- try {
804
- const response = {};
805
- for (const field of validation) {
806
- const key = _.compact([path, field.key]).join(`.`);
807
- _.set(response, key, options.original ? field.original : field.value);
808
- }
809
- return response;
810
- } catch (e) { throw e; }
811
- };
812
-
813
- export const extract = async (validation: Field<ValidationExtended>[], options: { original?: boolean } = {}): Promise<Record<string, any>> => {
814
- try {
815
- return await extract_recursive(validation, options);
816
- } catch (e) { throw e; }
817
166
  };