@beseif-solutions/prow-core 1.0.1 → 1.0.3
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/dist/entities/credentials.d.ts +1 -1
- package/dist/entities/credentials.js +2 -2
- package/dist/entities/events.d.ts +1 -1
- package/dist/entities/events.js +2 -2
- package/dist/entities/source.d.ts +1 -1
- package/dist/entities/source.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -6
- package/dist/minified-utils/context.js +1 -1
- package/dist/minified-utils/fields.d.ts +16 -46
- package/dist/minified-utils/fields.js +0 -615
- package/dist/minified-utils/validation.d.ts +31 -0
- package/dist/minified-utils/validation.js +626 -0
- package/package.json +1 -1
- package/src/entities/credentials.ts +2 -1
- package/src/entities/events.ts +2 -1
- package/src/entities/source.ts +2 -1
- package/src/index.ts +2 -1
- package/src/minified-utils/context.ts +5 -1
- package/src/minified-utils/fields.ts +17 -668
- package/src/minified-utils/validation.ts +666 -0
|
@@ -0,0 +1,666 @@
|
|
|
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)
|
|
509
|
+
const resolved = context.resolve(newField, value, options.context);
|
|
510
|
+
_.set(data, relativeKey, resolved);
|
|
511
|
+
|
|
512
|
+
// omit undefined for array mappings
|
|
513
|
+
if (typeof resolved === `undefined` && options.array) { return { schema: null, field: null }; }
|
|
514
|
+
|
|
515
|
+
// get schema for field with data
|
|
516
|
+
let calculatedSchema: Joi.Schema;
|
|
517
|
+
if (newField.as_array) {
|
|
518
|
+
const noArrayField = _.omit(newField, [`as_array`, `array_min`, `array_max`, `array_length`]) as Field;
|
|
519
|
+
|
|
520
|
+
if (resolved) {
|
|
521
|
+
const itemSchemas: Joi.Schema[] = [];
|
|
522
|
+
if (_.isArray(resolved)) {
|
|
523
|
+
for (let i = 0; i < (resolved || []).length; i++) {
|
|
524
|
+
const { schema: itemSchema } = await recursive_schema(noArrayField, data, { alter: false, relative: `${relativeKey}[${i}]`, array: true, context: options.context });
|
|
525
|
+
if (itemSchema) { itemSchemas.push(itemSchema); }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// override array value - omit undefined items
|
|
529
|
+
const withoutUndefined = resolved.filter((r) => typeof r !== `undefined`);
|
|
530
|
+
_.set(data, relativeKey, withoutUndefined);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
calculatedSchema = getOrderedArrayFieldSchema(newField, itemSchemas);
|
|
534
|
+
} else {
|
|
535
|
+
let genericItemSchema: Joi.Schema;
|
|
536
|
+
if (noArrayField.type === `dict` && noArrayField.items) {
|
|
537
|
+
const items: { key: string, schema: Joi.Schema, field: Field }[] = [];
|
|
538
|
+
|
|
539
|
+
for (const item of noArrayField.items) {
|
|
540
|
+
const recursive = await recursive_schema(item, data, { alter: options.alter && true, relative: `${relativeKey}[0]`, context: options.context });
|
|
541
|
+
if (recursive) { items.push({ key: item.key, schema: recursive.schema, field: recursive.field }); }
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
genericItemSchema = getObjectFieldSchema(noArrayField, items);
|
|
545
|
+
noArrayField.items = items.map((i) => i.field);
|
|
546
|
+
} else {
|
|
547
|
+
genericItemSchema = getSimpleFieldSchema(noArrayField);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
calculatedSchema = getItemsArrayFieldSchema(newField, [genericItemSchema]);
|
|
551
|
+
}
|
|
552
|
+
} else if (newField.type === `dict` && newField.items) {
|
|
553
|
+
if (resolved || newField.required) {
|
|
554
|
+
const items: { key: string, schema: Joi.Schema, field: Field }[] = [];
|
|
555
|
+
|
|
556
|
+
for (const item of newField.items) {
|
|
557
|
+
const recursive = await recursive_schema(item, data, { alter: options.alter && true, relative: relativeKey, context: options.context });
|
|
558
|
+
if (recursive) { items.push({ key: item.key, schema: recursive.schema, field: recursive.field }); }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
calculatedSchema = getObjectFieldSchema(newField, items);
|
|
562
|
+
newField.items = items.map((i) => i.field);
|
|
563
|
+
} else {
|
|
564
|
+
delete newField.items;
|
|
565
|
+
calculatedSchema = getSimpleFieldSchema(newField);
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
calculatedSchema = getSimpleFieldSchema(newField);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!calculatedSchema) { throw new Error(`Could not resolve schema for field ${newField.key}`); }
|
|
572
|
+
|
|
573
|
+
// if disabled schema validation is done but the value is not set
|
|
574
|
+
if (newField.disabled) { _.unset(data, relativeKey); }
|
|
575
|
+
|
|
576
|
+
return { schema: calculatedSchema, field: field.type === `file` ? alteredField : newField };
|
|
577
|
+
} catch (e) { throw e; }
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const validate_internal = async (fields: Field[], data: Record<string, any>, options: { secure: boolean, break: boolean, context: Record<string, any> }): Promise<ValidationResult> => {
|
|
581
|
+
try {
|
|
582
|
+
const response: ValidationResult<{}> = { valid: true, fields: [] };
|
|
583
|
+
|
|
584
|
+
for (const field of fields) {
|
|
585
|
+
|
|
586
|
+
const value = _.get(data, field.key, field.default);
|
|
587
|
+
|
|
588
|
+
const cloned = _.cloneDeep(data);
|
|
589
|
+
|
|
590
|
+
const recursive = await recursive_schema(field, cloned, { alter: true, context: options.context });
|
|
591
|
+
if (!recursive) { continue; }
|
|
592
|
+
|
|
593
|
+
const resolved = _.get(cloned, field.key, field.default);
|
|
594
|
+
|
|
595
|
+
let validation: any;
|
|
596
|
+
let error: any;
|
|
597
|
+
try {
|
|
598
|
+
validation = await recursive.schema.validateAsync(resolved, { abortEarly: false });
|
|
599
|
+
} catch (e) { error = e; }
|
|
600
|
+
|
|
601
|
+
let extended: ValidationExtended;
|
|
602
|
+
if (error) {
|
|
603
|
+
extended = {
|
|
604
|
+
valid: false,
|
|
605
|
+
errors: error.details ? error.details.map((d) => ({
|
|
606
|
+
type: d.type,
|
|
607
|
+
path: convertPath([field.key, ...d.path]),
|
|
608
|
+
message: d.message,
|
|
609
|
+
})) : [
|
|
610
|
+
{
|
|
611
|
+
type: `unknown`,
|
|
612
|
+
path: field.key,
|
|
613
|
+
message: error.message,
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
original: value,
|
|
617
|
+
value: null,
|
|
618
|
+
};
|
|
619
|
+
} else {
|
|
620
|
+
_.set(data, field.key, validation);
|
|
621
|
+
extended = {
|
|
622
|
+
valid: true,
|
|
623
|
+
original: value,
|
|
624
|
+
value: validation,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
response.valid &&= error ? false : true;
|
|
629
|
+
response.fields.push({
|
|
630
|
+
...recursive.field as any,
|
|
631
|
+
...options.secure ? await secure(field, extended) : extended,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return response;
|
|
637
|
+
} catch (e) { throw e; }
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export const validate = async <I>(fields: Field<I>[], data: Record<string, any>, options: { secure?: boolean, break?: boolean, context?: Record<string, any> } = {}): Promise<ValidationResult<I>> => {
|
|
641
|
+
try {
|
|
642
|
+
const { error } = fieldsSchema().validate(fields);
|
|
643
|
+
if (error) { throw new Error(`Invalid fields definition: ${error.message}`); }
|
|
644
|
+
const defaultOptions = _.assign({ secure: true, break: false, context: {} }, options);
|
|
645
|
+
const response = await validate_internal(fields, data, defaultOptions);
|
|
646
|
+
return response as any;
|
|
647
|
+
} catch (e) { throw e; }
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const extract_recursive = async (validation: Field<ValidationExtended>[], options: { original?: boolean }, path = ``): Promise<Record<string, any>> => {
|
|
651
|
+
try {
|
|
652
|
+
const response = {};
|
|
653
|
+
for (const field of validation) {
|
|
654
|
+
const key = _.compact([path, field.key]).join(`.`);
|
|
655
|
+
_.set(response, key, options.original ? field.original : field.value);
|
|
656
|
+
}
|
|
657
|
+
// remove undefined values
|
|
658
|
+
return JSON.parse(JSON.stringify(response));
|
|
659
|
+
} catch (e) { throw e; }
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
export const extract = async (validation: Field<ValidationExtended>[], options: { original?: boolean } = {}): Promise<Record<string, any>> => {
|
|
663
|
+
try {
|
|
664
|
+
return await extract_recursive(validation, options);
|
|
665
|
+
} catch (e) { throw e; }
|
|
666
|
+
};
|