@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.
@@ -1,6 +1,6 @@
1
+ import { Field } from "./fields";
1
2
  declare class Context {
2
- constructor();
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
- if (lodash_1.default.isObject(source) || lodash_1.default.isArray(source)) {
17
- isObject = true;
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 template = handlebars_1.default.compile(unresolved);
30
- const resolved = template(data);
31
- if (resolved.includes(`[object Object]`)) {
32
- const clonedData = lodash_1.default.cloneDeep(data);
33
- const parsed = handlebars_1.default.parseWithoutProcessing(unresolved);
34
- for (const x of parsed.body) {
35
- if (x.type === `MustacheStatement`) {
36
- const get = lodash_1.default.get(clonedData, x[`path`].original);
37
- if (lodash_1.default.isObject(get) || lodash_1.default.isArray(get)) {
38
- if (isObject) {
39
- unresolved = unresolved.replace(`"{{${x[`path`].original}}}"`, JSON.stringify(get));
40
- }
41
- else {
42
- lodash_1.default.set(clonedData, `${x[`path`].original}.toString`, () => JSON.stringify(get));
43
- if (x[`escaped`]) {
44
- unresolved = unresolved.replace(`{{${x[`path`].original}}}`, `{{{${x[`path`].original}}}}`);
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
- return isObject ? JSON.parse(resolved) : resolved;
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 getFieldSchema = (field) => {
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
- objSchema = getFieldsSchema(field.items).unknown(field.unknown || false);
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.alternatives(objSchema, joi_1.default.string().external(async (value, helpers) => {
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 = joi_1.default.alternatives(fileSchema(), joi_1.default.string().external(async (value, helpers) => {
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
- if (field.as_array) {
331
- let arraySchema = joi_1.default.array().items(typeSchema);
332
- if (field.array_length) {
333
- arraySchema = arraySchema.length(field.array_length);
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
- if (field.disabled) {
354
- typeSchema = typeSchema.forbidden();
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
- return field.required ? typeSchema.required() : typeSchema;
345
+ if (field.array_max) {
346
+ schema = schema.max(field.array_max);
347
+ }
348
+ return getGeneralSchema(field, schema);
357
349
  };
358
- const getFieldsSchema = (fields) => {
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 field of fields) {
361
- const fSchema = getFieldSchema(field);
362
- schema = schema.concat(joi_1.default.object({ [field.key]: fSchema }));
358
+ for (const item of items) {
359
+ schema = schema.concat(joi_1.default.object({
360
+ [item.key]: item.schema,
361
+ }));
363
362
  }
364
- return schema;
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 validate_internal = async (fields, data, options) => {
376
+ const relative_condition = (condition, relative) => {
369
377
  try {
370
- const response = { valid: true, fields: [] };
371
- const clonedData = lodash_1.default.cloneDeep(data);
372
- for (const field of lodash_1.default.compact(fields)) {
373
- if (options.break && !response.valid) {
374
- break;
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
- if (field.altered_by?.length > 0) {
377
- let hide = false;
378
- for (const alteration of field.altered_by) {
379
- const matches = (0, where_1.where)(clonedData, [alteration.condition]);
380
- if (matches) {
381
- if (`overrides` in alteration) {
382
- lodash_1.default.assign(field, alteration.overrides);
383
- }
384
- else if (`show` in alteration) {
385
- hide = !alteration.show;
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
- delete field.altered_by;
390
- if (hide) {
391
- continue;
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
- const value = lodash_1.default.get(clonedData, field.key, field.default);
395
- const resolved = context_1.default.resolve(value, options.context);
396
- const schema = getFieldSchema(field);
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,
@@ -248,7 +248,7 @@ const processWhere = (json, conditions, flags) => {
248
248
  matchesWhere && (matchesWhere = processWhere(value, [cond], flags));
249
249
  }
250
250
  else {
251
- continue;
251
+ matchesWhere && (matchesWhere = false);
252
252
  }
253
253
  if (!matchesWhere) {
254
254
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beseif-solutions/prow-core",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "description": "Prow core functionalities and classes",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -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
- constructor() {
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
- if (_.isObject(source) || _.isArray(source)) {
40
- isObject = true;
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 template = Handlebars.compile(unresolved);
49
- const resolved = template(data);
50
-
51
- if (resolved.includes(`[object Object]`)) {
52
- const clonedData = _.cloneDeep(data);
53
- // overwrite toString method
54
- const parsed = Handlebars.parseWithoutProcessing(unresolved);
55
- for (const x of parsed.body) {
56
- if (x.type === `MustacheStatement`) {
57
- const get = _.get(clonedData, x[`path`].original);
58
- if (_.isObject(get) || _.isArray(get)) {
59
- if (isObject) {
60
- unresolved = unresolved.replace(`"{{${x[`path`].original}}}"`, JSON.stringify(get));
61
- } else {
62
- _.set(clonedData, `${x[`path`].original}.toString`, () => JSON.stringify(get));
63
- if (x[`escaped`]) { unresolved = unresolved.replace(`{{${x[`path`].original}}}`, `{{{${x[`path`].original}}}}`); }
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
- return isObject ? JSON.parse(resolved) : resolved;
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 getFieldSchema = (field: Field) => {
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
- let objSchema: Joi.ObjectSchema;
490
- if (field.items) {
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 = Joi.alternatives(
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
- if (field.as_array) {
540
- let arraySchema = Joi.array().items(typeSchema);
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
- const parsed = JSON.parse(value);
553
- const { value: arrValue, error } = arraySchema.validate(parsed, { abortEarly: false });
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
- return arrValue;
557
- })
558
- );
559
- }
540
+ let schema = Joi.array().ordered(...items);
560
541
 
561
- if (field.disabled) { typeSchema = typeSchema.forbidden(); }
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.required ? typeSchema.required() : typeSchema;
546
+ return getGeneralSchema(field, schema);
564
547
  };
565
548
 
566
- const getFieldsSchema = (fields: Field[]) => {
567
- let schema = Joi.object();
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
- for (const field of fields) {
570
- const fSchema = getFieldSchema(field);
571
- schema = schema.concat(Joi.object({ [field.key]: fSchema }));
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
- return schema;
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 validate_internal = async (fields: Field[], data: Record<string, any>, options: { secure: boolean, break: boolean, context: Record<string, any> }): Promise<ValidationResult> => {
581
+ const relative_condition = (condition: Where, relative: string): typeof condition => {
589
582
  try {
590
- const response: ValidationResult<{}> = { valid: true, fields: [] };
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
- const clonedData = _.cloneDeep(data);
593
-
594
- for (const field of _.compact(fields)) {
595
- if (options.break && !response.valid) { break; }
596
-
597
- // alterations over data
598
- if (field.altered_by?.length > 0) {
599
- let hide = false;
600
- for (const alteration of field.altered_by) {
601
- const matches = where(clonedData, [alteration.condition]);
602
- if (matches) {
603
- if (`overrides` in alteration) {
604
- _.assign(field, alteration.overrides);
605
- } else if (`show` in alteration) {
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
- delete field.altered_by;
661
+ delete newField.altered_by;
612
662
 
613
- if (hide) { continue; }
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 value = _.get(clonedData, field.key, field.default);
684
+ const schema = await recursive_schema(field, cloned, { context: options.context });
617
685
 
618
- // resolve value with context
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
- continue;
316
+ // TODO: what is this case?
317
+ matchesWhere &&= false;
318
318
  }
319
319
 
320
320
  if (!matchesWhere) { break; }