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