@beseif-solutions/prow-core 0.0.38 → 0.1.0

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.
@@ -26,8 +26,11 @@ type EventConfig = Pick<CoreConfig, `credentials` | `outputs`> & {
26
26
  outputs: string[];
27
27
  };
28
28
  sources?: string[];
29
+ fields?: Record<string, any>;
29
30
  };
30
- type CommonEvents<Config extends EventConfig = {}> = Common<Config> & {
31
+ type CommonEvents<Config extends EventConfig = {
32
+ fields: Record<string, any>;
33
+ }> = Common<Config> & {
31
34
  model: `event`;
32
35
  categorization: {
33
36
  department: string;
@@ -49,7 +52,9 @@ export type Request = {
49
52
  query: Record<string, string>;
50
53
  body: any;
51
54
  };
52
- export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Config> & {
55
+ export type ImmediateTrigger<Config extends EventConfig = {
56
+ fields: Record<string, any>;
57
+ }> = CommonEvents<Config> & {
53
58
  type: `trigger`;
54
59
  immediate: true;
55
60
  outputs: [DefaultConnectableEvent<Config[`flags`][`outputs`][number]>];
@@ -57,7 +62,7 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
57
62
  mount?: CoreFunction<{
58
63
  credentials: Config[`credentials`];
59
64
  inputs: {
60
- fields: Record<string, any>;
65
+ fields: Config[`fields`];
61
66
  link: string;
62
67
  };
63
68
  outputs: {
@@ -68,7 +73,7 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
68
73
  unmount?: CoreFunction<{
69
74
  credentials: Config[`credentials`];
70
75
  inputs: {
71
- fields: Record<string, any>;
76
+ fields: Config[`fields`];
72
77
  link: string;
73
78
  setup?: any;
74
79
  };
@@ -79,7 +84,7 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
79
84
  challenge?: CoreFunction<{
80
85
  credentials: Config[`credentials`];
81
86
  inputs: {
82
- fields: Record<string, any>;
87
+ fields: Config[`fields`];
83
88
  link: string;
84
89
  request: Request;
85
90
  setup?: any;
@@ -92,7 +97,7 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
92
97
  handle: CoreFunction<{
93
98
  credentials: Config[`credentials`];
94
99
  inputs: {
95
- fields: Record<string, any>;
100
+ fields: Config[`fields`];
96
101
  request: Request;
97
102
  setup?: any;
98
103
  };
@@ -104,7 +109,7 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
104
109
  fire?: CoreFunction<{
105
110
  credentials: Config[`credentials`];
106
111
  inputs: {
107
- fields: Record<string, any>;
112
+ fields: Config[`fields`];
108
113
  link: string;
109
114
  setup?: any;
110
115
  };
@@ -115,7 +120,7 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
115
120
  poll?: CoreFunction<{
116
121
  credentials: Config[`credentials`];
117
122
  inputs: {
118
- fields: Record<string, any>;
123
+ fields: Config[`fields`];
119
124
  last_schedule?: string;
120
125
  limit?: number;
121
126
  };
@@ -126,7 +131,9 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
126
131
  }>;
127
132
  };
128
133
  };
129
- export type ScheduledTrigger<Config extends EventConfig = {}> = CommonEvents<Config> & {
134
+ export type ScheduledTrigger<Config extends EventConfig = {
135
+ fields: Record<string, any>;
136
+ }> = CommonEvents<Config> & {
130
137
  type: `trigger`;
131
138
  immediate: false;
132
139
  outputs: [DefaultConnectableEvent<Config[`flags`][`outputs`][number]>];
@@ -134,7 +141,7 @@ export type ScheduledTrigger<Config extends EventConfig = {}> = CommonEvents<Con
134
141
  poll: CoreFunction<{
135
142
  credentials: Config[`credentials`];
136
143
  inputs: {
137
- fields: Record<string, any>;
144
+ fields: Config[`fields`];
138
145
  last_schedule?: string;
139
146
  limit?: number;
140
147
  };
@@ -145,8 +152,12 @@ export type ScheduledTrigger<Config extends EventConfig = {}> = CommonEvents<Con
145
152
  }>;
146
153
  };
147
154
  };
148
- export type Trigger<Config extends EventConfig = {}> = ImmediateTrigger<Config> | ScheduledTrigger<Config>;
149
- export type Action<Config extends EventConfig = {}> = CommonEvents<Config> & {
155
+ export type Trigger<Config extends EventConfig = {
156
+ fields: Record<string, any>;
157
+ }> = ImmediateTrigger<Config> | ScheduledTrigger<Config>;
158
+ export type Action<Config extends EventConfig = {
159
+ fields: Record<string, any>;
160
+ }> = CommonEvents<Config> & {
150
161
  type: `action`;
151
162
  inputs: [DefaultConnectableEvent<Config[`flags`][`inputs`][number]>, ...AdditionalConnectableEvent<Config[`flags`][`inputs`][number]>[]];
152
163
  outputs: [DefaultConnectableEvent<Config[`flags`][`outputs`][number]>, ...(AdditionalConnectableEvent<Config[`flags`][`outputs`][number]> | ErrorConnectableEvent<Config[`flags`][`outputs`][number]>)[]];
@@ -154,7 +165,7 @@ export type Action<Config extends EventConfig = {}> = CommonEvents<Config> & {
154
165
  execute: CoreFunction<{
155
166
  credentials: Config[`credentials`];
156
167
  inputs: {
157
- fields: Record<string, any>;
168
+ fields: Config[`fields`];
158
169
  flags: Partial<Record<Config[`flags`][`inputs`][number], true>>;
159
170
  };
160
171
  outputs: {
@@ -164,7 +175,9 @@ export type Action<Config extends EventConfig = {}> = CommonEvents<Config> & {
164
175
  }>;
165
176
  };
166
177
  };
167
- export type Event<Config extends EventConfig = {}> = Action<Config> | Trigger<Config>;
178
+ export type Event<Config extends EventConfig = {
179
+ fields: Record<string, any>;
180
+ }> = Action<Config> | Trigger<Config>;
168
181
  export declare const scheduledTriggerSchema: () => Joi.ObjectSchema<any>;
169
182
  export declare const immediateTriggerSchema: () => Joi.ObjectSchema<any>;
170
183
  export declare const triggerSchema: () => Joi.AlternativesSchema<any>;
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { Credentials, credentialsSchema, OAuthCredentials, oauthCredentialsSchem
4
4
  export { Action, actionOutputSchema, actionSchema, Event, eventSchema, ImmediateTrigger, immediateTriggerSchema, Request, ScheduledTrigger, scheduledTriggerSchema, Trigger, triggerOutputSchema, triggerSchema } from './entities/events';
5
5
  export { Provider, providerSchema } from './entities/provider';
6
6
  export { Source } from './entities/source';
7
- export { extract, Field, fieldSchema, fieldsSchema, File, SelectFieldChoice, validate, ValidationResult } from './minified-utils/fields';
7
+ export { alter, extract, Field, fieldSchema, fieldsSchema, File, SelectFieldChoice, validate, ValidationResult } from './minified-utils/fields';
8
8
  export { i18n, LocalizedField, LocalizedProperties, localizeFields, localizeGroup, localizeInputs, localizeOutputs, localizeProperties } from './minified-utils/locales';
9
9
  export { Properties } from './minified-utils/properties';
10
10
  export { Protocol } from './minified-utils/protocol';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Protocol = exports.Properties = exports.localizeProperties = exports.localizeOutputs = exports.localizeInputs = exports.localizeGroup = exports.localizeFields = exports.i18n = exports.validate = exports.fieldsSchema = exports.fieldSchema = exports.extract = exports.providerSchema = exports.triggerSchema = exports.triggerOutputSchema = exports.scheduledTriggerSchema = exports.immediateTriggerSchema = exports.eventSchema = exports.actionSchema = exports.actionOutputSchema = exports.staticCredentialsSchema = exports.sessionCredentialsSchema = exports.oauthCredentialsSchema = exports.credentialsSchema = exports.idSchema = exports.core = void 0;
3
+ exports.Protocol = exports.Properties = exports.localizeProperties = exports.localizeOutputs = exports.localizeInputs = exports.localizeGroup = exports.localizeFields = exports.i18n = exports.validate = exports.fieldsSchema = exports.fieldSchema = exports.extract = exports.alter = exports.providerSchema = exports.triggerSchema = exports.triggerOutputSchema = exports.scheduledTriggerSchema = exports.immediateTriggerSchema = exports.eventSchema = exports.actionSchema = exports.actionOutputSchema = exports.staticCredentialsSchema = exports.sessionCredentialsSchema = exports.oauthCredentialsSchema = exports.credentialsSchema = exports.idSchema = exports.core = void 0;
4
4
  var core_1 = require("./core");
5
5
  Object.defineProperty(exports, "core", { enumerable: true, get: function () { return core_1.core; } });
6
6
  var commons_1 = require("./entities/commons");
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "triggerSchema", { enumerable: true, get: functio
21
21
  var provider_1 = require("./entities/provider");
22
22
  Object.defineProperty(exports, "providerSchema", { enumerable: true, get: function () { return provider_1.providerSchema; } });
23
23
  var fields_1 = require("./minified-utils/fields");
24
+ Object.defineProperty(exports, "alter", { enumerable: true, get: function () { return fields_1.alter; } });
24
25
  Object.defineProperty(exports, "extract", { enumerable: true, get: function () { return fields_1.extract; } });
25
26
  Object.defineProperty(exports, "fieldSchema", { enumerable: true, get: function () { return fields_1.fieldSchema; } });
26
27
  Object.defineProperty(exports, "fieldsSchema", { enumerable: true, get: function () { return fields_1.fieldsSchema; } });
@@ -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();
@@ -1,18 +1,16 @@
1
1
  import Joi from "joi";
2
- import { OneOf } from "./types";
3
2
  import { Where } from "./where";
4
3
  export type Condition = {
5
4
  condition: Where;
6
- } & OneOf<{
7
5
  overrides: Partial<Field>;
8
- show: boolean;
9
- }>;
6
+ };
10
7
  type Common = {
11
8
  key: string;
12
9
  altered_by?: Condition[];
13
10
  disabled?: boolean;
14
11
  required?: boolean;
15
12
  as_password?: boolean;
13
+ show?: boolean;
16
14
  show_label?: boolean;
17
15
  } & ({
18
16
  as_array?: false;
@@ -30,6 +28,7 @@ type Any = string | number | boolean | Any[] | {
30
28
  type AnyInputField<Extend = Record<never, never>> = {
31
29
  type: `any`;
32
30
  default?: Any | Any[];
31
+ allow_nullish?: boolean;
33
32
  } & (SelectField<Any, Extend> | {});
34
33
  type StringInputField<Extend = Record<never, never>> = {
35
34
  type: `string`;
@@ -127,6 +126,7 @@ export type ValidationResult<Extended = Record<string, never>> = {
127
126
  valid: boolean;
128
127
  fields: Field<Extended & ValidationExtended>[];
129
128
  };
129
+ export declare const alter: (field: Field, relative: string, data: Record<string, any>) => Field<Record<never, never>>;
130
130
  export declare const validate: <I>(fields: Field<I>[], data: Record<string, any>, options?: {
131
131
  secure?: boolean;
132
132
  break?: boolean;
@@ -3,23 +3,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.extract = exports.validate = exports.fieldsSchema = exports.fieldSchema = void 0;
6
+ exports.extract = exports.validate = exports.alter = exports.fieldsSchema = exports.fieldSchema = void 0;
7
7
  const joi_1 = __importDefault(require("joi"));
8
8
  const lodash_1 = __importDefault(require("lodash"));
9
9
  const moment_timezone_1 = __importDefault(require("moment-timezone"));
10
10
  const context_1 = __importDefault(require("./context"));
11
11
  const where_1 = require("./where");
12
12
  const conditionSchema = () => joi_1.default.object({
13
- condition: joi_1.default.object(),
14
- overrides: joi_1.default.object(),
15
- show: joi_1.default.bool(),
16
- }).xor(`overrides`, `show`);
13
+ condition: joi_1.default.object().required(),
14
+ overrides: joi_1.default.object().required(),
15
+ });
17
16
  const commonFieldSchema = () => joi_1.default.object({
18
17
  key: joi_1.default.string().required(),
19
18
  altered_by: joi_1.default.array().items(conditionSchema()),
20
19
  disabled: joi_1.default.bool(),
21
20
  required: joi_1.default.bool(),
22
21
  as_password: joi_1.default.bool(),
22
+ show: joi_1.default.bool(),
23
23
  show_label: joi_1.default.bool().default(true),
24
24
  as_array: joi_1.default.bool(),
25
25
  array_min: joi_1.default.when(`as_array`, {
@@ -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`, `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,11 +227,17 @@ 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`:
211
237
  typeSchema = anySchema();
238
+ if (field.allow_nullish) {
239
+ typeSchema = typeSchema.allow(null, ``, 0, undefined);
240
+ }
212
241
  break;
213
242
  case `boolean`:
214
243
  typeSchema = joi_1.default.bool();
@@ -285,37 +314,13 @@ const getFieldSchema = (field) => {
285
314
  }
286
315
  break;
287
316
  case `dict`:
288
- let objSchema;
289
317
  if (field.items) {
290
- objSchema = getFieldsSchema(field.items).unknown(field.unknown || false);
291
- }
292
- else {
293
- objSchema = joi_1.default.object().unknown(true);
318
+ throw new Error(`No simple field`);
294
319
  }
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
- }));
320
+ typeSchema = joi_1.default.object().unknown(true);
306
321
  break;
307
322
  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
- }));
323
+ typeSchema = fileSchema();
319
324
  break;
320
325
  }
321
326
  if (`format` in field && field.format === `select`) {
@@ -327,77 +332,162 @@ const getFieldSchema = (field) => {
327
332
  }
328
333
  }
329
334
  }
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
- }));
335
+ return getGeneralSchema(field, typeSchema);
336
+ };
337
+ const getArrayFieldSchema = (field, items) => {
338
+ if (!field.as_array) {
339
+ throw new Error(`as_array required for array schema generator`);
352
340
  }
353
- if (field.disabled) {
354
- typeSchema = typeSchema.forbidden();
341
+ let schema = joi_1.default.array().ordered(...items);
342
+ if (field.array_length) {
343
+ schema = schema.length(field.array_length);
355
344
  }
356
- return field.required ? typeSchema.required() : typeSchema;
345
+ if (field.array_min) {
346
+ schema = schema.min(field.array_min);
347
+ }
348
+ if (field.array_max) {
349
+ schema = schema.max(field.array_max);
350
+ }
351
+ return getGeneralSchema(field, schema);
357
352
  };
358
- const getFieldsSchema = (fields) => {
353
+ const getObjectFieldSchema = (field, items) => {
354
+ if (field.type !== `dict`) {
355
+ throw new Error(`type must be dict for object schema generator`);
356
+ }
357
+ if (!field.items) {
358
+ throw new Error(`items required for object schema generator`);
359
+ }
359
360
  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 }));
361
+ for (const item of items) {
362
+ schema = schema.concat(joi_1.default.object({
363
+ [item.key]: item.schema,
364
+ }));
365
+ }
366
+ if (field.unknown) {
367
+ schema = schema.unknown(field.unknown);
368
+ }
369
+ return getGeneralSchema(field, schema);
370
+ };
371
+ const getGeneralSchema = (field, schema) => {
372
+ if (field.required) {
373
+ schema = schema.required();
374
+ }
375
+ if (field.disabled) {
376
+ schema = schema.forbidden();
363
377
  }
364
378
  return schema;
365
379
  };
366
380
  const convertPath = (parts) => parts.map((p, i) => typeof p === `string` ? (i === 0 ? p : `.${p}`) : `[${p}]`).join(``);
367
381
  const secure = async (field, validation) => field.as_password ? lodash_1.default.omit(validation, [`value`, `original`]) : validation;
368
- const validate_internal = async (fields, data, options) => {
382
+ const relative_condition = (condition, relative) => {
369
383
  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;
384
+ const newCondition = lodash_1.default.cloneDeep(condition);
385
+ for (const key of Object.keys(newCondition)) {
386
+ if (key.startsWith(`@relative`)) {
387
+ const newKey = key.replace(`@relative`, relative ? relative : ``);
388
+ newCondition[newKey] = newCondition[key];
389
+ delete newCondition[key];
375
390
  }
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
- }
391
+ }
392
+ return newCondition;
393
+ }
394
+ catch (e) {
395
+ throw e;
396
+ }
397
+ };
398
+ const alter = (field, relative, data) => {
399
+ const cloned = lodash_1.default.cloneDeep(field);
400
+ for (const alteration of cloned.altered_by) {
401
+ const condition = relative_condition(alteration.condition, relative);
402
+ const matches = (0, where_1.where)(data, [condition]);
403
+ if (matches) {
404
+ lodash_1.default.assign(cloned, alteration.overrides);
405
+ }
406
+ }
407
+ return cloned;
408
+ };
409
+ exports.alter = alter;
410
+ const recursive_schema = async (field, data, options) => {
411
+ try {
412
+ let newField = lodash_1.default.cloneDeep(field);
413
+ if (newField.altered_by?.length > 0) {
414
+ newField = (0, exports.alter)(newField, options.relative, data);
415
+ if (options.alter) {
416
+ delete newField.altered_by;
417
+ }
418
+ }
419
+ const alteredField = lodash_1.default.cloneDeep(newField);
420
+ if (newField.type === `file`) {
421
+ newField = fileReplacer(newField);
422
+ }
423
+ const relativeKey = options.relative ?
424
+ options.relative.endsWith(`]`) ?
425
+ options.array ? options.relative : `${options.relative}.${newField.key}`
426
+ : `${options.relative}.${newField.key}`
427
+ : newField.key;
428
+ const value = lodash_1.default.get(data, relativeKey, newField.default);
429
+ const resolved = context_1.default.resolve(newField, value, options.context);
430
+ lodash_1.default.set(data, relativeKey, resolved);
431
+ let calculatedSchema;
432
+ if (newField.as_array && resolved) {
433
+ const itemSchemas = [];
434
+ if (lodash_1.default.isArray(resolved)) {
435
+ for (let i = 0; i < (resolved || []).length; i++) {
436
+ const { schema: itemSchema } = await recursive_schema(lodash_1.default.omit(newField, [`as_array`, `array_min`, `array_max`, `array_length`]), data, { alter: false, relative: `${relativeKey}[${i}]`, array: true, context: options.context });
437
+ if (itemSchema) {
438
+ itemSchemas.push(itemSchema);
387
439
  }
388
440
  }
389
- delete field.altered_by;
390
- if (hide) {
391
- continue;
441
+ }
442
+ calculatedSchema = getArrayFieldSchema(newField, itemSchemas);
443
+ }
444
+ else if (newField.type === `dict` && newField.items) {
445
+ const items = [];
446
+ if (lodash_1.default.isObject(resolved)) {
447
+ for (const item of newField.items) {
448
+ const recursive = await recursive_schema(item, data, { alter: options.alter && true, relative: relativeKey, context: options.context });
449
+ if (recursive) {
450
+ items.push({ key: item.key, schema: recursive.schema, field: recursive.field });
451
+ }
392
452
  }
393
453
  }
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);
454
+ calculatedSchema = getObjectFieldSchema(newField, items);
455
+ newField.items = items.map((i) => i.field);
456
+ }
457
+ else {
458
+ calculatedSchema = getSimpleFieldSchema(newField);
459
+ }
460
+ if (!calculatedSchema) {
461
+ throw new Error(`Could not resolve schema for field ${newField.key}`);
462
+ }
463
+ if (typeof newField.show === `boolean` && !newField.show) {
464
+ lodash_1.default.unset(data, relativeKey);
465
+ return;
466
+ }
467
+ else if (newField.disabled) {
468
+ lodash_1.default.unset(data, relativeKey);
469
+ }
470
+ return { schema: calculatedSchema, field: field.type === `file` ? alteredField : newField };
471
+ }
472
+ catch (e) {
473
+ throw e;
474
+ }
475
+ };
476
+ const validate_internal = async (fields, data, options) => {
477
+ try {
478
+ const response = { valid: true, fields: [] };
479
+ for (const field of fields) {
480
+ const value = lodash_1.default.get(data, field.key, field.default);
481
+ const cloned = lodash_1.default.cloneDeep(data);
482
+ const recursive = await recursive_schema(field, cloned, { alter: true, context: options.context });
483
+ if (!recursive) {
484
+ continue;
485
+ }
486
+ const resolved = lodash_1.default.get(cloned, field.key, field.default);
397
487
  let validation;
398
488
  let error;
399
489
  try {
400
- validation = await schema.validateAsync(resolved, { abortEarly: false });
490
+ validation = await recursive.schema.validateAsync(resolved, { abortEarly: false });
401
491
  }
402
492
  catch (e) {
403
493
  error = e;
@@ -422,10 +512,9 @@ const validate_internal = async (fields, data, options) => {
422
512
  value: validation,
423
513
  };
424
514
  }
425
- lodash_1.default.set(clonedData, field.key, validation);
426
515
  response.valid && (response.valid = error ? false : true);
427
516
  response.fields.push({
428
- ...field,
517
+ ...recursive.field,
429
518
  ...options.secure ? await secure(field, extended) : extended,
430
519
  });
431
520
  }
@@ -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.1.0",
4
4
  "description": "Prow core functionalities and classes",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -33,9 +33,10 @@ type EventConfig = Pick<CoreConfig, `credentials` | `outputs`> & {
33
33
  outputs: string[],
34
34
  },
35
35
  sources?: string[],
36
+ fields?: Record<string, any>,
36
37
  };
37
38
 
38
- type CommonEvents<Config extends EventConfig = {}> = Common<Config> & {
39
+ type CommonEvents<Config extends EventConfig = { fields: Record<string, any> }> = Common<Config> & {
39
40
  model: `event`,
40
41
  categorization: {
41
42
  department: string,
@@ -59,75 +60,75 @@ export type Request = {
59
60
  body: any,
60
61
  };
61
62
 
62
- export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Config> & {
63
+ export type ImmediateTrigger<Config extends EventConfig = { fields: Record<string, any> }> = CommonEvents<Config> & {
63
64
  type: `trigger`,
64
65
  immediate: true,
65
66
  outputs: [DefaultConnectableEvent<Config[`flags`][`outputs`][number]>],
66
67
  functions: {
67
68
  mount?: CoreFunction<{
68
69
  credentials: Config[`credentials`],
69
- inputs: { fields: Record<string, any>, link: string },
70
+ inputs: { fields: Config[`fields`], link: string },
70
71
  outputs: { done: boolean, setup?: any },
71
72
  }>,
72
73
  unmount?: CoreFunction<{
73
74
  credentials: Config[`credentials`],
74
- inputs: { fields: Record<string, any>, link: string, setup?: any },
75
+ inputs: { fields: Config[`fields`], link: string, setup?: any },
75
76
  outputs: { done: boolean },
76
77
  }>,
77
78
  challenge?: CoreFunction<{
78
79
  credentials: Config[`credentials`],
79
- inputs: { fields: Record<string, any>, link: string, request: Request, setup?: any },
80
+ inputs: { fields: Config[`fields`], link: string, request: Request, setup?: any },
80
81
  outputs: { done: boolean, response?: any },
81
82
  }>,
82
83
  handle: CoreFunction<{
83
84
  credentials: Config[`credentials`],
84
- inputs: { fields: Record<string, any>, request: Request, setup?: any },
85
+ inputs: { fields: Config[`fields`], request: Request, setup?: any },
85
86
  outputs: { records: Config[`outputs`][], flags: Partial<Record<Config[`flags`][`outputs`][number], true>> },
86
87
  }>,
87
88
  fire?: CoreFunction<{
88
89
  credentials: Config[`credentials`],
89
- inputs: { fields: Record<string, any>, link: string, setup?: any },
90
+ inputs: { fields: Config[`fields`], link: string, setup?: any },
90
91
  outputs: { done: boolean },
91
92
  }>,
92
93
  poll?: CoreFunction<{
93
94
  credentials: Config[`credentials`],
94
- inputs: { fields: Record<string, any>, last_schedule?: string, limit?: number },
95
+ inputs: { fields: Config[`fields`], last_schedule?: string, limit?: number },
95
96
  outputs: { records: Config[`outputs`][], flags: Partial<Record<Config[`flags`][`outputs`][number], true>> },
96
97
  }>,
97
98
  },
98
99
  };
99
100
 
100
- export type ScheduledTrigger<Config extends EventConfig = {}> = CommonEvents<Config> & {
101
+ export type ScheduledTrigger<Config extends EventConfig = { fields: Record<string, any> }> = CommonEvents<Config> & {
101
102
  type: `trigger`,
102
103
  immediate: false,
103
104
  outputs: [DefaultConnectableEvent<Config[`flags`][`outputs`][number]>],
104
105
  functions: {
105
106
  poll: CoreFunction<{
106
107
  credentials: Config[`credentials`],
107
- inputs: { fields: Record<string, any>, last_schedule?: string, limit?: number },
108
+ inputs: { fields: Config[`fields`], last_schedule?: string, limit?: number },
108
109
  outputs: { records: Config[`outputs`][], flags: Partial<Record<Config[`flags`][`outputs`][number], true>> },
109
110
  }>,
110
111
  },
111
112
  };
112
113
 
113
- export type Trigger<Config extends EventConfig = {}> =
114
+ export type Trigger<Config extends EventConfig = { fields: Record<string, any> }> =
114
115
  | ImmediateTrigger<Config>
115
116
  | ScheduledTrigger<Config>;
116
117
 
117
- export type Action<Config extends EventConfig = {}> = CommonEvents<Config> & {
118
+ export type Action<Config extends EventConfig = { fields: Record<string, any> }> = CommonEvents<Config> & {
118
119
  type: `action`,
119
120
  inputs: [DefaultConnectableEvent<Config[`flags`][`inputs`][number]>, ...AdditionalConnectableEvent<Config[`flags`][`inputs`][number]>[]],
120
121
  outputs: [DefaultConnectableEvent<Config[`flags`][`outputs`][number]>, ...(AdditionalConnectableEvent<Config[`flags`][`outputs`][number]> | ErrorConnectableEvent<Config[`flags`][`outputs`][number]>)[]],
121
122
  functions: {
122
123
  execute: CoreFunction<{
123
124
  credentials: Config[`credentials`],
124
- inputs: { fields: Record<string, any>, flags: Partial<Record<Config[`flags`][`inputs`][number], true>> },
125
+ inputs: { fields: Config[`fields`], flags: Partial<Record<Config[`flags`][`inputs`][number], true>> },
125
126
  outputs: { output: Config[`outputs`], flags: Partial<Record<Config[`flags`][`outputs`][number], true>> },
126
127
  }>,
127
128
  },
128
129
  };
129
130
 
130
- export type Event<Config extends EventConfig = {}> =
131
+ export type Event<Config extends EventConfig = { fields: Record<string, any> }> =
131
132
  | Action<Config>
132
133
  | Trigger<Config>;
133
134
 
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ export { Credentials, credentialsSchema, OAuthCredentials, oauthCredentialsSchem
4
4
  export { Action, actionOutputSchema, actionSchema, Event, eventSchema, ImmediateTrigger, immediateTriggerSchema, Request, ScheduledTrigger, scheduledTriggerSchema, Trigger, triggerOutputSchema, triggerSchema } from './entities/events';
5
5
  export { Provider, providerSchema } from './entities/provider';
6
6
  export { Source } from './entities/source';
7
- export { extract, Field, fieldSchema, fieldsSchema, File, SelectFieldChoice, validate, ValidationResult } from './minified-utils/fields';
7
+ export { alter, extract, Field, fieldSchema, fieldsSchema, File, SelectFieldChoice, validate, ValidationResult } from './minified-utils/fields';
8
8
  export { i18n, LocalizedField, LocalizedProperties, localizeFields, localizeGroup, localizeInputs, localizeOutputs, localizeProperties } from './minified-utils/locales';
9
9
  export { Properties } from './minified-utils/properties';
10
10
  export { Protocol } from './minified-utils/protocol';
@@ -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
  }
@@ -2,15 +2,12 @@ import Joi, { DateSchema, NumberSchema, StringSchema } from "joi";
2
2
  import _ from "lodash";
3
3
  import moment from "moment-timezone";
4
4
  import context from "./context";
5
- import { OneOf } from "./types";
6
5
  import { where, Where } from "./where";
7
6
 
8
7
  export type Condition = {
9
8
  condition: Where,
10
- } & OneOf<{
11
9
  overrides: Partial<Field>,
12
- show: boolean,
13
- }>;
10
+ };
14
11
 
15
12
  type Common = {
16
13
  key: string,
@@ -18,6 +15,7 @@ type Common = {
18
15
  disabled?: boolean,
19
16
  required?: boolean,
20
17
  as_password?: boolean,
18
+ show?: boolean,
21
19
  show_label?: boolean,
22
20
  } & ({
23
21
  as_array?: false,
@@ -47,6 +45,7 @@ type Any = string | number | boolean | Any[] | { [key: string]: Any };
47
45
  type AnyInputField<Extend = Record<never, never>> = {
48
46
  type: `any`,
49
47
  default?: Any | Any[],
48
+ allow_nullish?: boolean, // allow null values
50
49
  } & (
51
50
  | SelectField<Any, Extend>
52
51
  | {}
@@ -181,10 +180,9 @@ type ValidationExtended = {
181
180
 
182
181
  const conditionSchema = () =>
183
182
  Joi.object({
184
- condition: Joi.object(),
185
- overrides: Joi.object(),
186
- show: Joi.bool(),
187
- }).xor(`overrides`, `show`);
183
+ condition: Joi.object().required(),
184
+ overrides: Joi.object().required(),
185
+ });
188
186
 
189
187
  const commonFieldSchema = () =>
190
188
  Joi.object({
@@ -193,6 +191,7 @@ const commonFieldSchema = () =>
193
191
  disabled: Joi.bool(),
194
192
  required: Joi.bool(),
195
193
  as_password: Joi.bool(),
194
+ show: Joi.bool(),
196
195
  show_label: Joi.bool().default(true),
197
196
  as_array: Joi.bool(),
198
197
  array_min: Joi.when(`as_array`, {
@@ -363,6 +362,31 @@ const fileSchema = () => Joi.object({
363
362
  content: Joi.string().required(), // base64 encoded content
364
363
  });
365
364
 
365
+ // ÑAPA: translate file to structured dict
366
+ const fileReplacer = (field: Common & FileInputField): Field => ({
367
+ key: field.key,
368
+ type: `dict`,
369
+ items: [
370
+ {
371
+ key: `name`,
372
+ type: `string`,
373
+ required: true,
374
+ },
375
+ {
376
+ key: `type`,
377
+ type: `string`,
378
+ required: true,
379
+ },
380
+ {
381
+ key: `content`,
382
+ type: `string`,
383
+ required: true,
384
+ },
385
+ ],
386
+ default: field.default,
387
+ ..._.pick(field, [`altered_by`, `disabled`, `required`, `as_password`, `show`, `show_label`, `as_array`, `array_min`, `array_max`, `array_length`]),
388
+ }) as Field;
389
+
366
390
  const fileInputFieldSchema = () =>
367
391
  Joi.object({
368
392
  default: defaultSchema(fileSchema()),
@@ -425,11 +449,15 @@ export const fieldSchema = () =>
425
449
  export const fieldsSchema = () =>
426
450
  Joi.array().items(fieldSchema());
427
451
 
428
- const getFieldSchema = (field: Field) => {
452
+ const getSimpleFieldSchema = (field: Field) => {
453
+ if (field.as_array) { throw new Error(`Cannot use as_array with simple field schema generator`); }
454
+
429
455
  let typeSchema: Joi.Schema;
430
456
  switch (field.type) {
431
457
  case `any`:
432
458
  typeSchema = anySchema();
459
+ // allow null values for any type
460
+ if (field.allow_nullish) { typeSchema = typeSchema.allow(null, ``, 0, undefined); }
433
461
  break;
434
462
  case `boolean`:
435
463
  typeSchema = Joi.bool();
@@ -486,43 +514,11 @@ const getFieldSchema = (field: Field) => {
486
514
  }
487
515
  break;
488
516
  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
-
517
+ if (field.items) { throw new Error(`No simple field`); }
518
+ typeSchema = Joi.object().unknown(true);
510
519
  break;
511
520
  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
-
521
+ typeSchema = fileSchema();
526
522
  break;
527
523
  }
528
524
 
@@ -536,41 +532,40 @@ const getFieldSchema = (field: Field) => {
536
532
  }
537
533
  }
538
534
 
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; }
535
+ return getGeneralSchema(field, typeSchema);
536
+ };
551
537
 
552
- const parsed = JSON.parse(value);
553
- const { value: arrValue, error } = arraySchema.validate(parsed, { abortEarly: false });
554
- if (error) { throw error; }
538
+ const getArrayFieldSchema = (field: Field, items: Joi.Schema[]) => {
539
+ if (!field.as_array) { throw new Error(`as_array required for array schema generator`); }
555
540
 
556
- return arrValue;
557
- })
558
- );
559
- }
541
+ let schema = Joi.array().ordered(...items);
560
542
 
561
- if (field.disabled) { typeSchema = typeSchema.forbidden(); }
543
+ if (field.array_length) { schema = schema.length(field.array_length); }
544
+ if (field.array_min) { schema = schema.min(field.array_min); }
545
+ if (field.array_max) { schema = schema.max(field.array_max); }
562
546
 
563
- return field.required ? typeSchema.required() : typeSchema;
547
+ return getGeneralSchema(field, schema);
564
548
  };
565
549
 
566
- const getFieldsSchema = (fields: Field[]) => {
567
- let schema = Joi.object();
550
+ const getObjectFieldSchema = (field: Field, items: { key: string, schema: Joi.Schema }[]) => {
551
+ if (field.type !== `dict`) { throw new Error(`type must be dict for object schema generator`); }
552
+ if (!field.items) { throw new Error(`items required for object schema generator`); }
568
553
 
569
- for (const field of fields) {
570
- const fSchema = getFieldSchema(field);
571
- schema = schema.concat(Joi.object({ [field.key]: fSchema }));
554
+ let schema = Joi.object();
555
+ for (const item of items) {
556
+ schema = schema.concat(Joi.object({
557
+ [item.key]: item.schema,
558
+ }));
572
559
  }
573
560
 
561
+ if (field.unknown) { schema = schema.unknown(field.unknown); }
562
+
563
+ return getGeneralSchema(field, schema);
564
+ };
565
+
566
+ const getGeneralSchema = (field: Field, schema: Joi.Schema): Joi.Schema => {
567
+ if (field.required) { schema = schema.required(); }
568
+ if (field.disabled) { schema = schema.forbidden(); }
574
569
  return schema;
575
570
  };
576
571
 
@@ -585,45 +580,125 @@ export type ValidationResult<Extended = Record<string, never>> = {
585
580
  fields: Field<Extended & ValidationExtended>[],
586
581
  };
587
582
 
588
- const validate_internal = async (fields: Field[], data: Record<string, any>, options: { secure: boolean, break: boolean, context: Record<string, any> }): Promise<ValidationResult> => {
583
+ const relative_condition = (condition: Where, relative: string): typeof condition => {
589
584
  try {
590
- const response: ValidationResult<{}> = { valid: true, fields: [] };
585
+ const newCondition = _.cloneDeep(condition);
586
+ for (const key of Object.keys(newCondition)) {
587
+ if (key.startsWith(`@relative`)) {
588
+ const newKey = key.replace(`@relative`, relative ? relative : ``);
589
+ newCondition[newKey] = newCondition[key];
590
+ delete newCondition[key];
591
+ }
592
+ }
593
+
594
+ return newCondition;
595
+ } catch (e) { throw e; }
596
+ };
597
+
598
+ export const alter = (field: Field, relative: string, data: Record<string, any>) => {
599
+ const cloned = _.cloneDeep(field);
600
+ for (const alteration of cloned.altered_by) {
601
+ const condition = relative_condition(alteration.condition, relative);
602
+
603
+ const matches = where(data, [condition]);
604
+ if (matches) { _.assign(cloned, alteration.overrides); }
605
+ }
606
+ return cloned;
607
+ };
608
+
609
+ 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 }> => {
610
+ try {
611
+ let newField: Field = _.cloneDeep(field);
612
+
613
+ // alterations over already resolved data (previous fields)
614
+ if (newField.altered_by?.length > 0) {
615
+ newField = alter(newField, options.relative, data);
591
616
 
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
- }
608
- }
617
+ // remove altered_by only if it's not an array -> array alterations must be kept for front-end rendering
618
+ if (options.alter) { delete newField.altered_by; }
619
+ }
620
+
621
+ const alteredField: Field = _.cloneDeep(newField);
622
+
623
+ // ÑAPA: if the field is a file, replace it
624
+ if (newField.type === `file`) { newField = fileReplacer(newField); }
625
+
626
+ const relativeKey = options.relative ?
627
+ options.relative.endsWith(`]`) ?
628
+ options.array ? options.relative : `${options.relative}.${newField.key}`
629
+ : `${options.relative}.${newField.key}`
630
+ : newField.key;
631
+
632
+ const value = _.get(data, relativeKey, newField.default);
633
+
634
+ // resolve value with context (even if it's not valid)
635
+ const resolved = context.resolve(newField, value, options.context);
636
+ _.set(data, relativeKey, resolved);
637
+
638
+ // get schema for field with data
639
+ let calculatedSchema: Joi.Schema;
640
+ if (newField.as_array && resolved) {
641
+ const itemSchemas: Joi.Schema[] = [];
642
+ if (_.isArray(resolved)) {
643
+ for (let i = 0; i < (resolved || []).length; i++) {
644
+ const { schema: itemSchema } = await recursive_schema(
645
+ _.omit(newField, [`as_array`, `array_min`, `array_max`, `array_length`]) as Field,
646
+ data, { alter: false, relative: `${relativeKey}[${i}]`, array: true, context: options.context });
647
+ if (itemSchema) { itemSchemas.push(itemSchema); }
609
648
  }
649
+ }
610
650
 
611
- delete field.altered_by;
651
+ calculatedSchema = getArrayFieldSchema(newField, itemSchemas);
652
+ } else if (newField.type === `dict` && newField.items) {
653
+ const items: { key: string, schema: Joi.Schema, field: Field }[] = [];
612
654
 
613
- if (hide) { continue; }
655
+ if (_.isObject(resolved)) {
656
+ for (const item of newField.items) {
657
+ const recursive = await recursive_schema(item, data, { alter: options.alter && true, relative: relativeKey, context: options.context });
658
+ if (recursive) { items.push({ key: item.key, schema: recursive.schema, field: recursive.field }); }
659
+ }
614
660
  }
615
661
 
616
- const value = _.get(clonedData, field.key, field.default);
662
+ calculatedSchema = getObjectFieldSchema(newField, items);
663
+ newField.items = items.map((i) => i.field);
664
+ } else {
665
+ calculatedSchema = getSimpleFieldSchema(newField);
666
+ }
667
+
668
+ if (!calculatedSchema) { throw new Error(`Could not resolve schema for field ${newField.key}`); }
669
+
670
+ if (typeof newField.show === `boolean` && !newField.show) {
671
+ // if the field is hidden, we don't need to validate it
672
+ _.unset(data, relativeKey);
673
+ return;
674
+ } else if (newField.disabled) {
675
+ // TODO: check this
676
+ _.unset(data, relativeKey);
677
+ }
678
+
679
+ return { schema: calculatedSchema, field: field.type === `file` ? alteredField : newField };
680
+ } catch (e) { throw e; }
681
+ };
682
+
683
+ const validate_internal = async (fields: Field[], data: Record<string, any>, options: { secure: boolean, break: boolean, context: Record<string, any> }): Promise<ValidationResult> => {
684
+ try {
685
+ const response: ValidationResult<{}> = { valid: true, fields: [] };
686
+
687
+ for (const field of fields) {
688
+
689
+ const value = _.get(data, field.key, field.default);
690
+
691
+ const cloned = _.cloneDeep(data);
692
+
693
+ const recursive = await recursive_schema(field, cloned, { alter: true, context: options.context });
694
+ if (!recursive) { continue; }
617
695
 
618
- // resolve value with context
619
- const resolved = context.resolve(value, options.context);
696
+ const resolved = _.get(cloned, field.key, field.default);
620
697
 
621
- // validate resolved value
622
- const schema = getFieldSchema(field);
623
698
  let validation: any;
624
699
  let error: any;
625
700
  try {
626
- validation = await schema.validateAsync(resolved, { abortEarly: false });
701
+ validation = await recursive.schema.validateAsync(resolved, { abortEarly: false });
627
702
  } catch (e) { error = e; }
628
703
 
629
704
  let extended: ValidationExtended;
@@ -646,10 +721,9 @@ const validate_internal = async (fields: Field[], data: Record<string, any>, opt
646
721
  };
647
722
  }
648
723
 
649
- _.set(clonedData, field.key, validation);
650
724
  response.valid &&= error ? false : true;
651
725
  response.fields.push({
652
- ...field as any,
726
+ ...recursive.field as any,
653
727
  ...options.secure ? await secure(field, extended) : extended,
654
728
  });
655
729
 
@@ -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; }
package/tsconfig.json CHANGED
@@ -11,10 +11,9 @@
11
11
  "declaration": true
12
12
  },
13
13
  "include": [
14
- "src/**/*.ts",
14
+ "src/**/*.ts"
15
15
  ],
16
16
  "exclude": [
17
- "./test",
18
17
  "node_modules",
19
18
  "dist/**/*.js"
20
19
  ]