@beseif-solutions/prow-core 0.0.38 → 0.0.39
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/minified-utils/context.d.ts +2 -2
- package/dist/minified-utils/context.js +36 -43
- package/dist/minified-utils/fields.js +152 -82
- package/dist/minified-utils/where.js +1 -1
- package/package.json +1 -1
- package/src/minified-utils/context.ts +39 -54
- package/src/minified-utils/fields.ts +153 -89
- package/src/minified-utils/where.ts +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Field } from "./fields";
|
|
1
2
|
declare class Context {
|
|
2
|
-
|
|
3
|
-
readonly resolve: (source: any, data: Record<string, any>) => any;
|
|
3
|
+
readonly resolve: (field: Field, source: any, data: Record<string, any>) => any;
|
|
4
4
|
}
|
|
5
5
|
declare const context: Context;
|
|
6
6
|
export default context;
|
|
@@ -7,14 +7,14 @@ const handlebars_1 = __importDefault(require("handlebars"));
|
|
|
7
7
|
const lodash_1 = __importDefault(require("lodash"));
|
|
8
8
|
class Context {
|
|
9
9
|
constructor() {
|
|
10
|
-
this.resolve = (source, data) => {
|
|
11
|
-
let isObject = false;
|
|
12
|
-
let unresolved;
|
|
10
|
+
this.resolve = (field, source, data) => {
|
|
13
11
|
if (lodash_1.default.isNull(source) || lodash_1.default.isUndefined(source)) {
|
|
14
12
|
return source;
|
|
15
13
|
}
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
let serialized = false;
|
|
15
|
+
let unresolved;
|
|
16
|
+
if ((lodash_1.default.isObject(source) || lodash_1.default.isArray(source)) && field.type === `dict` && !field.items) {
|
|
17
|
+
serialized = true;
|
|
18
18
|
unresolved = JSON.stringify(source);
|
|
19
19
|
}
|
|
20
20
|
else if (typeof source === `string`) {
|
|
@@ -26,50 +26,43 @@ class Context {
|
|
|
26
26
|
if (!unresolved || !unresolved.includes(`{{`)) {
|
|
27
27
|
return source;
|
|
28
28
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
29
|
+
const clonedData = lodash_1.default.cloneDeep(data);
|
|
30
|
+
let resolved = unresolved;
|
|
31
|
+
const process = handlebars_1.default.parseWithoutProcessing(unresolved);
|
|
32
|
+
for (const x of process.body) {
|
|
33
|
+
if (x.type === `MustacheStatement`) {
|
|
34
|
+
const path = x[`path`].original;
|
|
35
|
+
const expression = x[`escaped`] ? `{{${path}}}` : `{{{${path}}}}`;
|
|
36
|
+
const get = lodash_1.default.get(clonedData, path);
|
|
37
|
+
if (get) {
|
|
38
|
+
const shouldInfer = field.as_array && lodash_1.default.isArray(get)
|
|
39
|
+
|| field.type === `dict` && lodash_1.default.isObject(get)
|
|
40
|
+
|| field.type === `file` && lodash_1.default.isObject(get)
|
|
41
|
+
|| field.type === `number` && typeof get === `number`
|
|
42
|
+
|| field.type === `string` && typeof get === `string`
|
|
43
|
+
|| field.type === `boolean` && typeof get === `boolean`
|
|
44
|
+
|| field.type === `any`;
|
|
45
|
+
if (unresolved === expression && shouldInfer) {
|
|
46
|
+
resolved = get;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (serialized) {
|
|
51
|
+
resolved = resolved.replace(`"${expression}"`, lodash_1.default.isObject(get) ? JSON.stringify(get) : `"${get}"`);
|
|
46
52
|
}
|
|
53
|
+
resolved = resolved.replace(expression, get);
|
|
47
54
|
}
|
|
48
55
|
}
|
|
56
|
+
else {
|
|
57
|
+
resolved = resolved.replace(expression, ``);
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
|
-
return this.resolve(isObject ? JSON.parse(unresolved) : unresolved, clonedData);
|
|
51
60
|
}
|
|
52
|
-
|
|
61
|
+
const parsed = serialized ? JSON.parse(resolved) : resolved;
|
|
62
|
+
return (typeof resolved === `string` ? resolved : JSON.stringify(resolved)).includes(`{{`) ?
|
|
63
|
+
this.resolve(field, parsed, clonedData)
|
|
64
|
+
: parsed;
|
|
53
65
|
};
|
|
54
|
-
handlebars_1.default.registerHelper({
|
|
55
|
-
eq: (v1, v2) => v1 === v2,
|
|
56
|
-
ne: (v1, v2) => v1 !== v2,
|
|
57
|
-
lt: (v1, v2) => v1 < v2,
|
|
58
|
-
gt: (v1, v2) => v1 > v2,
|
|
59
|
-
lte: (v1, v2) => v1 <= v2,
|
|
60
|
-
gte: (v1, v2) => v1 >= v2,
|
|
61
|
-
and: (v1, v2) => v1 && v2,
|
|
62
|
-
or: (v1, v2) => v1 || v2,
|
|
63
|
-
"+": (v1, v2) => v1 + v2,
|
|
64
|
-
"-": (v1, v2) => v1 - v2,
|
|
65
|
-
"*": (v1, v2) => v1 * v2,
|
|
66
|
-
"/": (v1, v2) => v1 / v2,
|
|
67
|
-
"sum": (v1, v2) => v1 + v2,
|
|
68
|
-
"subtract": (v1, v2) => v1 - v2,
|
|
69
|
-
"multiply": (v1, v2) => v1 * v2,
|
|
70
|
-
"divide": (v1, v2) => v1 / v2,
|
|
71
|
-
"abs": (v1) => Math.abs(v1),
|
|
72
|
-
});
|
|
73
66
|
}
|
|
74
67
|
}
|
|
75
68
|
const context = new Context();
|
|
@@ -152,6 +152,29 @@ const fileSchema = () => joi_1.default.object({
|
|
|
152
152
|
type: joi_1.default.string().required(),
|
|
153
153
|
content: joi_1.default.string().required(),
|
|
154
154
|
});
|
|
155
|
+
const fileReplacer = (field) => ({
|
|
156
|
+
key: field.key,
|
|
157
|
+
type: `dict`,
|
|
158
|
+
items: [
|
|
159
|
+
{
|
|
160
|
+
key: `name`,
|
|
161
|
+
type: `string`,
|
|
162
|
+
required: true,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
key: `type`,
|
|
166
|
+
type: `string`,
|
|
167
|
+
required: true,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: `content`,
|
|
171
|
+
type: `string`,
|
|
172
|
+
required: true,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
default: field.default,
|
|
176
|
+
...lodash_1.default.pick(field, [`altered_by`, `disabled`, `required`, `as_password`, `show_label`, `as_array`, `array_min`, `array_max`, `array_length`]),
|
|
177
|
+
});
|
|
155
178
|
const fileInputFieldSchema = () => joi_1.default.object({
|
|
156
179
|
default: defaultSchema(fileSchema()),
|
|
157
180
|
accept: joi_1.default.alternatives(joi_1.default.string(), joi_1.default.array().items(joi_1.default.string())).optional(),
|
|
@@ -204,7 +227,10 @@ const fieldSchema = () => inputFieldBaseSchema()
|
|
|
204
227
|
exports.fieldSchema = fieldSchema;
|
|
205
228
|
const fieldsSchema = () => joi_1.default.array().items((0, exports.fieldSchema)());
|
|
206
229
|
exports.fieldsSchema = fieldsSchema;
|
|
207
|
-
const
|
|
230
|
+
const getSimpleFieldSchema = (field) => {
|
|
231
|
+
if (field.as_array) {
|
|
232
|
+
throw new Error(`Cannot use as_array with simple field schema generator`);
|
|
233
|
+
}
|
|
208
234
|
let typeSchema;
|
|
209
235
|
switch (field.type) {
|
|
210
236
|
case `any`:
|
|
@@ -285,37 +311,13 @@ const getFieldSchema = (field) => {
|
|
|
285
311
|
}
|
|
286
312
|
break;
|
|
287
313
|
case `dict`:
|
|
288
|
-
let objSchema;
|
|
289
314
|
if (field.items) {
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
objSchema = joi_1.default.object().unknown(true);
|
|
315
|
+
throw new Error(`No simple field`);
|
|
294
316
|
}
|
|
295
|
-
typeSchema = joi_1.default.
|
|
296
|
-
if (!helpers.original && !field.required) {
|
|
297
|
-
return helpers.original;
|
|
298
|
-
}
|
|
299
|
-
const parsed = JSON.parse(value);
|
|
300
|
-
const { value: objValue, error } = objSchema.validate(parsed, { abortEarly: false });
|
|
301
|
-
if (error) {
|
|
302
|
-
throw error;
|
|
303
|
-
}
|
|
304
|
-
return objValue;
|
|
305
|
-
}));
|
|
317
|
+
typeSchema = joi_1.default.object().unknown(true);
|
|
306
318
|
break;
|
|
307
319
|
case `file`:
|
|
308
|
-
typeSchema =
|
|
309
|
-
if (!helpers.original && !field.required) {
|
|
310
|
-
return helpers.original;
|
|
311
|
-
}
|
|
312
|
-
const parsed = JSON.parse(value);
|
|
313
|
-
const { value: fileValue, error } = fileSchema().validate(parsed, { abortEarly: false });
|
|
314
|
-
if (error) {
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
return fileValue;
|
|
318
|
-
}));
|
|
320
|
+
typeSchema = fileSchema();
|
|
319
321
|
break;
|
|
320
322
|
}
|
|
321
323
|
if (`format` in field && field.format === `select`) {
|
|
@@ -327,73 +329,142 @@ const getFieldSchema = (field) => {
|
|
|
327
329
|
}
|
|
328
330
|
}
|
|
329
331
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (field.array_min) {
|
|
336
|
-
arraySchema = arraySchema.min(field.array_min);
|
|
337
|
-
}
|
|
338
|
-
if (field.array_max) {
|
|
339
|
-
arraySchema = arraySchema.max(field.array_max);
|
|
340
|
-
}
|
|
341
|
-
typeSchema = joi_1.default.alternatives(arraySchema, joi_1.default.string().external(async (value, helpers) => {
|
|
342
|
-
if (!helpers.original && !field.required) {
|
|
343
|
-
return helpers.original;
|
|
344
|
-
}
|
|
345
|
-
const parsed = JSON.parse(value);
|
|
346
|
-
const { value: arrValue, error } = arraySchema.validate(parsed, { abortEarly: false });
|
|
347
|
-
if (error) {
|
|
348
|
-
throw error;
|
|
349
|
-
}
|
|
350
|
-
return arrValue;
|
|
351
|
-
}));
|
|
332
|
+
return getGeneralSchema(field, typeSchema);
|
|
333
|
+
};
|
|
334
|
+
const getArrayFieldSchema = (field, items) => {
|
|
335
|
+
if (!field.as_array) {
|
|
336
|
+
throw new Error(`as_array required for array schema generator`);
|
|
352
337
|
}
|
|
353
|
-
|
|
354
|
-
|
|
338
|
+
let schema = joi_1.default.array().ordered(...items);
|
|
339
|
+
if (field.array_length) {
|
|
340
|
+
schema = schema.length(field.array_length);
|
|
341
|
+
}
|
|
342
|
+
if (field.array_min) {
|
|
343
|
+
schema = schema.min(field.array_min);
|
|
355
344
|
}
|
|
356
|
-
|
|
345
|
+
if (field.array_max) {
|
|
346
|
+
schema = schema.max(field.array_max);
|
|
347
|
+
}
|
|
348
|
+
return getGeneralSchema(field, schema);
|
|
357
349
|
};
|
|
358
|
-
const
|
|
350
|
+
const getObjectFieldSchema = (field, items) => {
|
|
351
|
+
if (field.type !== `dict`) {
|
|
352
|
+
throw new Error(`type must be dict for object schema generator`);
|
|
353
|
+
}
|
|
354
|
+
if (!field.items) {
|
|
355
|
+
throw new Error(`items required for object schema generator`);
|
|
356
|
+
}
|
|
359
357
|
let schema = joi_1.default.object();
|
|
360
|
-
for (const
|
|
361
|
-
|
|
362
|
-
|
|
358
|
+
for (const item of items) {
|
|
359
|
+
schema = schema.concat(joi_1.default.object({
|
|
360
|
+
[item.key]: item.schema,
|
|
361
|
+
}));
|
|
363
362
|
}
|
|
364
|
-
|
|
363
|
+
if (field.unknown) {
|
|
364
|
+
schema = schema.unknown(field.unknown);
|
|
365
|
+
}
|
|
366
|
+
return getGeneralSchema(field, schema);
|
|
367
|
+
};
|
|
368
|
+
const getGeneralSchema = (field, schema) => {
|
|
369
|
+
if (field.disabled) {
|
|
370
|
+
schema = schema.forbidden();
|
|
371
|
+
}
|
|
372
|
+
return field.required ? schema.required() : schema;
|
|
365
373
|
};
|
|
366
374
|
const convertPath = (parts) => parts.map((p, i) => typeof p === `string` ? (i === 0 ? p : `.${p}`) : `[${p}]`).join(``);
|
|
367
375
|
const secure = async (field, validation) => field.as_password ? lodash_1.default.omit(validation, [`value`, `original`]) : validation;
|
|
368
|
-
const
|
|
376
|
+
const relative_condition = (condition, relative) => {
|
|
369
377
|
try {
|
|
370
|
-
const
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
378
|
+
const newCondition = lodash_1.default.cloneDeep(condition);
|
|
379
|
+
for (const key of Object.keys(newCondition)) {
|
|
380
|
+
if (key.startsWith(`@relative`)) {
|
|
381
|
+
const newKey = key.replace(`@relative`, relative ? relative : ``);
|
|
382
|
+
newCondition[newKey] = newCondition[key];
|
|
383
|
+
delete newCondition[key];
|
|
375
384
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
}
|
|
386
|
+
return newCondition;
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
throw e;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const recursive_schema = async (field, data, options) => {
|
|
393
|
+
try {
|
|
394
|
+
const newField = field.type === `file` ? fileReplacer(field) : lodash_1.default.cloneDeep(field);
|
|
395
|
+
const relativeKey = options.relative ?
|
|
396
|
+
options.relative.endsWith(`]`) ?
|
|
397
|
+
options.array ? options.relative : `${options.relative}.${newField.key}`
|
|
398
|
+
: `${options.relative}.${newField.key}`
|
|
399
|
+
: newField.key;
|
|
400
|
+
const value = lodash_1.default.get(data, relativeKey, newField.default);
|
|
401
|
+
const resolved = context_1.default.resolve(newField, value, options.context);
|
|
402
|
+
lodash_1.default.set(data, relativeKey, resolved);
|
|
403
|
+
let calculatedSchema;
|
|
404
|
+
if (newField.as_array && resolved) {
|
|
405
|
+
const itemSchemas = [];
|
|
406
|
+
if (lodash_1.default.isArray(resolved)) {
|
|
407
|
+
for (let i = 0; i < (resolved || []).length; i++) {
|
|
408
|
+
const itemSchema = await recursive_schema(lodash_1.default.omit(newField, [`as_array`, `array_min`, `array_max`, `array_length`]), data, { relative: `${relativeKey}[${i}]`, array: true, context: options.context });
|
|
409
|
+
if (itemSchema) {
|
|
410
|
+
itemSchemas.push(itemSchema);
|
|
387
411
|
}
|
|
388
412
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
413
|
+
}
|
|
414
|
+
calculatedSchema = getArrayFieldSchema(newField, itemSchemas);
|
|
415
|
+
}
|
|
416
|
+
else if (newField.type === `dict` && newField.items) {
|
|
417
|
+
const itemSchemas = [];
|
|
418
|
+
if (lodash_1.default.isObject(resolved)) {
|
|
419
|
+
for (const item of newField.items) {
|
|
420
|
+
const itemSchema = await recursive_schema(item, data, { relative: relativeKey, context: options.context });
|
|
421
|
+
if (itemSchema) {
|
|
422
|
+
itemSchemas.push({ key: item.key, schema: itemSchema });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
calculatedSchema = getObjectFieldSchema(newField, itemSchemas);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
calculatedSchema = getSimpleFieldSchema(newField);
|
|
430
|
+
}
|
|
431
|
+
if (!calculatedSchema) {
|
|
432
|
+
throw new Error(`Could not resolve schema for field ${newField.key}`);
|
|
433
|
+
}
|
|
434
|
+
if (newField.altered_by?.length > 0) {
|
|
435
|
+
let hide = false;
|
|
436
|
+
for (const alteration of newField.altered_by) {
|
|
437
|
+
const condition = relative_condition(alteration.condition, options.relative);
|
|
438
|
+
const matches = (0, where_1.where)(data, [condition]);
|
|
439
|
+
if (matches) {
|
|
440
|
+
if (`overrides` in alteration) {
|
|
441
|
+
lodash_1.default.assign(newField, alteration.overrides);
|
|
442
|
+
}
|
|
443
|
+
else if (`show` in alteration) {
|
|
444
|
+
hide = !alteration.show;
|
|
445
|
+
}
|
|
392
446
|
}
|
|
393
447
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
448
|
+
delete newField.altered_by;
|
|
449
|
+
if (hide) {
|
|
450
|
+
lodash_1.default.unset(data, relativeKey);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return calculatedSchema;
|
|
455
|
+
}
|
|
456
|
+
catch (e) {
|
|
457
|
+
throw e;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const validate_internal = async (fields, data, options) => {
|
|
461
|
+
try {
|
|
462
|
+
const response = { valid: true, fields: [] };
|
|
463
|
+
for (const field of fields) {
|
|
464
|
+
const value = lodash_1.default.get(data, field.key, field.default);
|
|
465
|
+
const cloned = lodash_1.default.cloneDeep(data);
|
|
466
|
+
const schema = await recursive_schema(field, cloned, { context: options.context });
|
|
467
|
+
const resolved = lodash_1.default.get(cloned, field.key, field.default);
|
|
397
468
|
let validation;
|
|
398
469
|
let error;
|
|
399
470
|
try {
|
|
@@ -422,7 +493,6 @@ const validate_internal = async (fields, data, options) => {
|
|
|
422
493
|
value: validation,
|
|
423
494
|
};
|
|
424
495
|
}
|
|
425
|
-
lodash_1.default.set(clonedData, field.key, validation);
|
|
426
496
|
response.valid && (response.valid = error ? false : true);
|
|
427
497
|
response.fields.push({
|
|
428
498
|
...field,
|
package/package.json
CHANGED
|
@@ -1,43 +1,16 @@
|
|
|
1
1
|
import Handlebars from "handlebars";
|
|
2
2
|
import _ from "lodash";
|
|
3
|
+
import { Field } from "./fields";
|
|
3
4
|
|
|
4
5
|
class Context {
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// logic and math helpers
|
|
9
|
-
Handlebars.registerHelper({
|
|
10
|
-
eq: (v1: any, v2: any) => v1 === v2,
|
|
11
|
-
ne: (v1: any, v2: any) => v1 !== v2,
|
|
12
|
-
lt: (v1: any, v2: any) => v1 < v2,
|
|
13
|
-
gt: (v1: any, v2: any) => v1 > v2,
|
|
14
|
-
lte: (v1: any, v2: any) => v1 <= v2,
|
|
15
|
-
gte: (v1: any, v2: any) => v1 >= v2,
|
|
16
|
-
and: (v1: any, v2: any) => v1 && v2,
|
|
17
|
-
or: (v1: any, v2: any) => v1 || v2,
|
|
18
|
-
"+": (v1: number, v2: number) => v1 + v2,
|
|
19
|
-
"-": (v1: number, v2: number) => v1 - v2,
|
|
20
|
-
"*": (v1: number, v2: number) => v1 * v2,
|
|
21
|
-
"/": (v1: number, v2: number) => v1 / v2,
|
|
22
|
-
"sum": (v1: number, v2: number) => v1 + v2,
|
|
23
|
-
"subtract": (v1: number, v2: number) => v1 - v2,
|
|
24
|
-
"multiply": (v1: number, v2: number) => v1 * v2,
|
|
25
|
-
"divide": (v1: number, v2: number) => v1 / v2,
|
|
26
|
-
"abs": (v1: number) => Math.abs(v1),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// TODO: register helpers
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
public readonly resolve = (source: any, data: Record<string, any>) => {
|
|
34
|
-
let isObject = false;
|
|
35
|
-
let unresolved: string;
|
|
36
|
-
|
|
7
|
+
public readonly resolve = (field: Field, source: any, data: Record<string, any>) => {
|
|
37
8
|
if (_.isNull(source) || _.isUndefined(source)) { return source; }
|
|
38
9
|
|
|
39
|
-
|
|
40
|
-
|
|
10
|
+
let serialized = false;
|
|
11
|
+
let unresolved: string;
|
|
12
|
+
if ((_.isObject(source) || _.isArray(source)) && field.type === `dict` && !field.items) {
|
|
13
|
+
serialized = true;
|
|
41
14
|
unresolved = JSON.stringify(source);
|
|
42
15
|
} else if (typeof source === `string`) {
|
|
43
16
|
unresolved = source;
|
|
@@ -45,32 +18,44 @@ class Context {
|
|
|
45
18
|
|
|
46
19
|
if (!unresolved || !unresolved.includes(`{{`)) { return source; }
|
|
47
20
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
21
|
+
const clonedData = _.cloneDeep(data);
|
|
22
|
+
|
|
23
|
+
let resolved: any = unresolved;
|
|
24
|
+
const process = Handlebars.parseWithoutProcessing(unresolved);
|
|
25
|
+
for (const x of process.body) {
|
|
26
|
+
if (x.type === `MustacheStatement`) {
|
|
27
|
+
const path = x[`path`].original;
|
|
28
|
+
const expression = x[`escaped`] ? `{{${path}}}` : `{{{${path}}}}`;
|
|
29
|
+
const get = _.get(clonedData, path);
|
|
30
|
+
|
|
31
|
+
if (get) {
|
|
32
|
+
const shouldInfer =
|
|
33
|
+
field.as_array && _.isArray(get)
|
|
34
|
+
|| field.type === `dict` && _.isObject(get)
|
|
35
|
+
|| field.type === `file` && _.isObject(get)
|
|
36
|
+
|| field.type === `number` && typeof get === `number`
|
|
37
|
+
|| field.type === `string` && typeof get === `string`
|
|
38
|
+
|| field.type === `boolean` && typeof get === `boolean`
|
|
39
|
+
|| field.type === `any`;
|
|
40
|
+
|
|
41
|
+
if (unresolved === expression && shouldInfer) {
|
|
42
|
+
resolved = get;
|
|
43
|
+
break;
|
|
44
|
+
} else {
|
|
45
|
+
if (serialized) { resolved = resolved.replace(`"${expression}"`, _.isObject(get) ? JSON.stringify(get) : `"${get}"`); }
|
|
46
|
+
resolved = resolved.replace(expression, get);
|
|
65
47
|
|
|
66
48
|
}
|
|
67
|
-
}
|
|
49
|
+
} else { resolved = resolved.replace(expression, ``); }
|
|
68
50
|
}
|
|
69
|
-
|
|
70
|
-
return this.resolve(isObject ? JSON.parse(unresolved) : unresolved, clonedData);
|
|
71
51
|
}
|
|
72
52
|
|
|
73
|
-
|
|
53
|
+
const parsed = serialized ? JSON.parse(resolved) : resolved;
|
|
54
|
+
|
|
55
|
+
// recursive resolution
|
|
56
|
+
return (typeof resolved === `string` ? resolved : JSON.stringify(resolved)).includes(`{{`) ?
|
|
57
|
+
this.resolve(field, parsed, clonedData)
|
|
58
|
+
: parsed;
|
|
74
59
|
};
|
|
75
60
|
|
|
76
61
|
}
|
|
@@ -363,6 +363,31 @@ const fileSchema = () => Joi.object({
|
|
|
363
363
|
content: Joi.string().required(), // base64 encoded content
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
+
// ÑAPA: translate file to structured dict
|
|
367
|
+
const fileReplacer = (field: Common & FileInputField): Field => ({
|
|
368
|
+
key: field.key,
|
|
369
|
+
type: `dict`,
|
|
370
|
+
items: [
|
|
371
|
+
{
|
|
372
|
+
key: `name`,
|
|
373
|
+
type: `string`,
|
|
374
|
+
required: true,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
key: `type`,
|
|
378
|
+
type: `string`,
|
|
379
|
+
required: true,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
key: `content`,
|
|
383
|
+
type: `string`,
|
|
384
|
+
required: true,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
default: field.default,
|
|
388
|
+
..._.pick(field, [`altered_by`, `disabled`, `required`, `as_password`, `show_label`, `as_array`, `array_min`, `array_max`, `array_length`]),
|
|
389
|
+
}) as Field;
|
|
390
|
+
|
|
366
391
|
const fileInputFieldSchema = () =>
|
|
367
392
|
Joi.object({
|
|
368
393
|
default: defaultSchema(fileSchema()),
|
|
@@ -425,7 +450,9 @@ export const fieldSchema = () =>
|
|
|
425
450
|
export const fieldsSchema = () =>
|
|
426
451
|
Joi.array().items(fieldSchema());
|
|
427
452
|
|
|
428
|
-
const
|
|
453
|
+
const getSimpleFieldSchema = (field: Field) => {
|
|
454
|
+
if (field.as_array) { throw new Error(`Cannot use as_array with simple field schema generator`); }
|
|
455
|
+
|
|
429
456
|
let typeSchema: Joi.Schema;
|
|
430
457
|
switch (field.type) {
|
|
431
458
|
case `any`:
|
|
@@ -486,43 +513,11 @@ const getFieldSchema = (field: Field) => {
|
|
|
486
513
|
}
|
|
487
514
|
break;
|
|
488
515
|
case `dict`:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
objSchema = getFieldsSchema(field.items).unknown(field.unknown || false);
|
|
492
|
-
} else {
|
|
493
|
-
objSchema = Joi.object().unknown(true);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
typeSchema = Joi.alternatives(
|
|
497
|
-
objSchema,
|
|
498
|
-
// support for stringified objects
|
|
499
|
-
Joi.string().external(async (value, helpers) => {
|
|
500
|
-
if (!helpers.original && !field.required) { return helpers.original; }
|
|
501
|
-
|
|
502
|
-
const parsed = JSON.parse(value);
|
|
503
|
-
const { value: objValue, error } = objSchema.validate(parsed, { abortEarly: false });
|
|
504
|
-
if (error) { throw error; }
|
|
505
|
-
|
|
506
|
-
return objValue;
|
|
507
|
-
})
|
|
508
|
-
);
|
|
509
|
-
|
|
516
|
+
if (field.items) { throw new Error(`No simple field`); }
|
|
517
|
+
typeSchema = Joi.object().unknown(true);
|
|
510
518
|
break;
|
|
511
519
|
case `file`:
|
|
512
|
-
typeSchema =
|
|
513
|
-
fileSchema(),
|
|
514
|
-
// support for stringified objects
|
|
515
|
-
Joi.string().external(async (value, helpers) => {
|
|
516
|
-
if (!helpers.original && !field.required) { return helpers.original; }
|
|
517
|
-
|
|
518
|
-
const parsed = JSON.parse(value);
|
|
519
|
-
const { value: fileValue, error } = fileSchema().validate(parsed, { abortEarly: false });
|
|
520
|
-
if (error) { throw error; }
|
|
521
|
-
|
|
522
|
-
return fileValue;
|
|
523
|
-
})
|
|
524
|
-
);
|
|
525
|
-
|
|
520
|
+
typeSchema = fileSchema();
|
|
526
521
|
break;
|
|
527
522
|
}
|
|
528
523
|
|
|
@@ -536,42 +531,40 @@ const getFieldSchema = (field: Field) => {
|
|
|
536
531
|
}
|
|
537
532
|
}
|
|
538
533
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (field.array_length) { arraySchema = arraySchema.length(field.array_length); }
|
|
543
|
-
if (field.array_min) { arraySchema = arraySchema.min(field.array_min); }
|
|
544
|
-
if (field.array_max) { arraySchema = arraySchema.max(field.array_max); }
|
|
545
|
-
|
|
546
|
-
typeSchema = Joi.alternatives(
|
|
547
|
-
arraySchema,
|
|
548
|
-
// support for stringified arrays
|
|
549
|
-
Joi.string().external(async (value, helpers) => {
|
|
550
|
-
if (!helpers.original && !field.required) { return helpers.original; }
|
|
534
|
+
return getGeneralSchema(field, typeSchema);
|
|
535
|
+
};
|
|
551
536
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (error) { throw error; }
|
|
537
|
+
const getArrayFieldSchema = (field: Field, items: Joi.Schema[]) => {
|
|
538
|
+
if (!field.as_array) { throw new Error(`as_array required for array schema generator`); }
|
|
555
539
|
|
|
556
|
-
|
|
557
|
-
})
|
|
558
|
-
);
|
|
559
|
-
}
|
|
540
|
+
let schema = Joi.array().ordered(...items);
|
|
560
541
|
|
|
561
|
-
if (field.
|
|
542
|
+
if (field.array_length) { schema = schema.length(field.array_length); }
|
|
543
|
+
if (field.array_min) { schema = schema.min(field.array_min); }
|
|
544
|
+
if (field.array_max) { schema = schema.max(field.array_max); }
|
|
562
545
|
|
|
563
|
-
return field
|
|
546
|
+
return getGeneralSchema(field, schema);
|
|
564
547
|
};
|
|
565
548
|
|
|
566
|
-
const
|
|
567
|
-
|
|
549
|
+
const getObjectFieldSchema = (field: Field, items: { key: string, schema: Joi.Schema }[]) => {
|
|
550
|
+
if (field.type !== `dict`) { throw new Error(`type must be dict for object schema generator`); }
|
|
551
|
+
if (!field.items) { throw new Error(`items required for object schema generator`); }
|
|
568
552
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
schema = schema.concat(Joi.object({
|
|
553
|
+
let schema = Joi.object();
|
|
554
|
+
for (const item of items) {
|
|
555
|
+
schema = schema.concat(Joi.object({
|
|
556
|
+
[item.key]: item.schema,
|
|
557
|
+
}));
|
|
572
558
|
}
|
|
573
559
|
|
|
574
|
-
|
|
560
|
+
if (field.unknown) { schema = schema.unknown(field.unknown); }
|
|
561
|
+
|
|
562
|
+
return getGeneralSchema(field, schema);
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const getGeneralSchema = (field: Field, schema: Joi.Schema): Joi.Schema => {
|
|
566
|
+
if (field.disabled) { schema = schema.forbidden(); }
|
|
567
|
+
return field.required ? schema.required() : schema;
|
|
575
568
|
};
|
|
576
569
|
|
|
577
570
|
const convertPath = (parts: (string | number)[]): string =>
|
|
@@ -585,41 +578,113 @@ export type ValidationResult<Extended = Record<string, never>> = {
|
|
|
585
578
|
fields: Field<Extended & ValidationExtended>[],
|
|
586
579
|
};
|
|
587
580
|
|
|
588
|
-
const
|
|
581
|
+
const relative_condition = (condition: Where, relative: string): typeof condition => {
|
|
589
582
|
try {
|
|
590
|
-
const
|
|
583
|
+
const newCondition = _.cloneDeep(condition);
|
|
584
|
+
for (const key of Object.keys(newCondition)) {
|
|
585
|
+
if (key.startsWith(`@relative`)) {
|
|
586
|
+
const newKey = key.replace(`@relative`, relative ? relative : ``);
|
|
587
|
+
newCondition[newKey] = newCondition[key];
|
|
588
|
+
delete newCondition[key];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return newCondition;
|
|
593
|
+
} catch (e) { throw e; }
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const recursive_schema = async (field: Field, data: Record<string, any>, options: { relative?: string, array?: boolean, context: Record<string, any> }): Promise<Joi.Schema> => {
|
|
597
|
+
try {
|
|
598
|
+
// ÑAPA: if the field is a file, replace it
|
|
599
|
+
const newField = field.type === `file` ? fileReplacer(field) : _.cloneDeep(field);
|
|
600
|
+
|
|
601
|
+
const relativeKey = options.relative ?
|
|
602
|
+
options.relative.endsWith(`]`) ?
|
|
603
|
+
options.array ? options.relative : `${options.relative}.${newField.key}`
|
|
604
|
+
: `${options.relative}.${newField.key}`
|
|
605
|
+
: newField.key;
|
|
606
|
+
|
|
607
|
+
const value = _.get(data, relativeKey, newField.default);
|
|
608
|
+
|
|
609
|
+
// resolve value with context (even if it's not valid)
|
|
610
|
+
const resolved = context.resolve(newField, value, options.context);
|
|
611
|
+
_.set(data, relativeKey, resolved);
|
|
612
|
+
|
|
613
|
+
// get schema for field with data
|
|
614
|
+
let calculatedSchema: Joi.Schema;
|
|
615
|
+
if (newField.as_array && resolved) {
|
|
616
|
+
|
|
617
|
+
const itemSchemas: Joi.Schema[] = [];
|
|
618
|
+
if (_.isArray(resolved)) {
|
|
619
|
+
for (let i = 0; i < (resolved || []).length; i++) {
|
|
620
|
+
const itemSchema = await recursive_schema(
|
|
621
|
+
_.omit(newField, [`as_array`, `array_min`, `array_max`, `array_length`]) as Field,
|
|
622
|
+
data, { relative: `${relativeKey}[${i}]`, array: true, context: options.context });
|
|
623
|
+
if (itemSchema) { itemSchemas.push(itemSchema); }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
calculatedSchema = getArrayFieldSchema(newField, itemSchemas);
|
|
628
|
+
} else if (newField.type === `dict` && newField.items) {
|
|
629
|
+
const itemSchemas: { key: string, schema: Joi.Schema }[] = [];
|
|
630
|
+
|
|
631
|
+
if (_.isObject(resolved)) {
|
|
632
|
+
for (const item of newField.items) {
|
|
633
|
+
const itemSchema = await recursive_schema(item, data, { relative: relativeKey, context: options.context });
|
|
634
|
+
if (itemSchema) { itemSchemas.push({ key: item.key, schema: itemSchema }); }
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
calculatedSchema = getObjectFieldSchema(newField, itemSchemas);
|
|
639
|
+
} else {
|
|
640
|
+
calculatedSchema = getSimpleFieldSchema(newField);
|
|
641
|
+
}
|
|
591
642
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
hide = !alteration.show;
|
|
607
|
-
}
|
|
643
|
+
if (!calculatedSchema) { throw new Error(`Could not resolve schema for field ${newField.key}`); }
|
|
644
|
+
|
|
645
|
+
// alterations over data
|
|
646
|
+
if (newField.altered_by?.length > 0) {
|
|
647
|
+
let hide = false;
|
|
648
|
+
for (const alteration of newField.altered_by) {
|
|
649
|
+
const condition = relative_condition(alteration.condition, options.relative);
|
|
650
|
+
|
|
651
|
+
const matches = where(data, [condition]);
|
|
652
|
+
if (matches) {
|
|
653
|
+
if (`overrides` in alteration) {
|
|
654
|
+
_.assign(newField, alteration.overrides);
|
|
655
|
+
} else if (`show` in alteration) {
|
|
656
|
+
hide = !alteration.show;
|
|
608
657
|
}
|
|
609
658
|
}
|
|
659
|
+
}
|
|
610
660
|
|
|
611
|
-
|
|
661
|
+
delete newField.altered_by;
|
|
612
662
|
|
|
613
|
-
|
|
663
|
+
if (hide) {
|
|
664
|
+
// if the field is hidden, we don't need to validate it
|
|
665
|
+
_.unset(data, relativeKey);
|
|
666
|
+
return;
|
|
614
667
|
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return calculatedSchema;
|
|
671
|
+
} catch (e) { throw e; }
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const validate_internal = async (fields: Field[], data: Record<string, any>, options: { secure: boolean, break: boolean, context: Record<string, any> }): Promise<ValidationResult> => {
|
|
675
|
+
try {
|
|
676
|
+
const response: ValidationResult<{}> = { valid: true, fields: [] };
|
|
677
|
+
|
|
678
|
+
for (const field of fields) {
|
|
679
|
+
|
|
680
|
+
const value = _.get(data, field.key, field.default);
|
|
681
|
+
|
|
682
|
+
const cloned = _.cloneDeep(data);
|
|
615
683
|
|
|
616
|
-
const
|
|
684
|
+
const schema = await recursive_schema(field, cloned, { context: options.context });
|
|
617
685
|
|
|
618
|
-
|
|
619
|
-
const resolved = context.resolve(value, options.context);
|
|
686
|
+
const resolved = _.get(cloned, field.key, field.default);
|
|
620
687
|
|
|
621
|
-
// validate resolved value
|
|
622
|
-
const schema = getFieldSchema(field);
|
|
623
688
|
let validation: any;
|
|
624
689
|
let error: any;
|
|
625
690
|
try {
|
|
@@ -646,7 +711,6 @@ const validate_internal = async (fields: Field[], data: Record<string, any>, opt
|
|
|
646
711
|
};
|
|
647
712
|
}
|
|
648
713
|
|
|
649
|
-
_.set(clonedData, field.key, validation);
|
|
650
714
|
response.valid &&= error ? false : true;
|
|
651
715
|
response.fields.push({
|
|
652
716
|
...field as any,
|
|
@@ -313,8 +313,8 @@ const processWhere = (json: Record<string, any>, conditions: Where[], flags?: Fl
|
|
|
313
313
|
} else if (isWhere(cond)) {
|
|
314
314
|
matchesWhere &&= processWhere(value, [cond], flags);
|
|
315
315
|
} else {
|
|
316
|
-
// TODO
|
|
317
|
-
|
|
316
|
+
// TODO: what is this case?
|
|
317
|
+
matchesWhere &&= false;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
if (!matchesWhere) { break; }
|