@beseif-solutions/prow-core 0.0.35 → 0.0.37

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/core.d.ts CHANGED
@@ -1,24 +1,25 @@
1
- import axios from "axios";
1
+ import { AxiosStatic } from "axios";
2
2
  import Joi from "joi";
3
- import _ from "lodash";
3
+ import { LoDashStatic } from "lodash";
4
4
  import moment from "moment-timezone";
5
5
  import { FromCatalog, FromDirectory, T } from "./minified-utils/locales";
6
- type SourceFunctions = {
7
- register: <T>(source: string, data: T) => Promise<{
6
+ type SyncOrAsync<T> = T | Promise<T>;
7
+ export type SourceFunctions = {
8
+ register: <T>(source: string, data: T) => SyncOrAsync<{
8
9
  id: string;
9
10
  }>;
10
- consume: <T>(source: string) => Promise<{
11
+ consume: <T>(source: string) => SyncOrAsync<{
11
12
  id: string;
12
13
  data: T;
13
14
  }[]>;
14
- unregister: (source: string, id: string) => Promise<void>;
15
+ unregister: (source: string, id: string) => SyncOrAsync<void>;
15
16
  };
16
17
  export type Core = {
17
- axios: typeof axios;
18
+ axios: AxiosStatic;
18
19
  moment: typeof moment;
19
- lodash: typeof _;
20
+ lodash: LoDashStatic;
20
21
  env: Record<string, any>;
21
- joi: typeof Joi;
22
+ joi: Joi.Root;
22
23
  sources?: SourceFunctions;
23
24
  t: T;
24
25
  };
@@ -76,6 +76,18 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
76
76
  done: boolean;
77
77
  };
78
78
  }>;
79
+ challenge?: CoreFunction<{
80
+ credentials: Config[`credentials`];
81
+ inputs: {
82
+ fields: Record<string, any>;
83
+ link: string;
84
+ setup?: any;
85
+ };
86
+ outputs: {
87
+ done: boolean;
88
+ response?: any;
89
+ };
90
+ }>;
79
91
  handle: CoreFunction<{
80
92
  credentials: Config[`credentials`];
81
93
  inputs: {
@@ -68,9 +68,10 @@ const immediateTriggerSchema = () => joi_1.default.object({
68
68
  functions: joi_1.default.object({
69
69
  mount: joi_1.default.function(),
70
70
  unmount: joi_1.default.function(),
71
- fire: joi_1.default.function(),
71
+ challenge: joi_1.default.function(),
72
72
  handle: joi_1.default.function().required(),
73
73
  poll: joi_1.default.function(),
74
+ fire: joi_1.default.function(),
74
75
  }).required(),
75
76
  })
76
77
  .concat((0, commons_1.commonSchema)())
@@ -4,6 +4,7 @@ import { Action, Trigger } from "./events";
4
4
  import { Source } from "./source";
5
5
  export type Provider = {
6
6
  id: string;
7
+ version: string;
7
8
  model: `provider`;
8
9
  credentials: Record<string, Credentials>;
9
10
  triggers: Record<string, Trigger>;
@@ -5,12 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.providerSchema = void 0;
7
7
  const joi_1 = __importDefault(require("joi"));
8
+ const commons_1 = require("./commons");
8
9
  const credentials_1 = require("./credentials");
9
10
  const events_1 = require("./events");
10
- const commons_1 = require("./commons");
11
11
  const source_1 = require("./source");
12
12
  const providerSchema = () => joi_1.default.object({
13
13
  id: (0, commons_1.idSchema)(),
14
+ version: joi_1.default.string().required(),
14
15
  model: joi_1.default.valid(`provider`).required(),
15
16
  credentials: joi_1.default.object()
16
17
  .pattern((0, commons_1.idSchema)(), (0, credentials_1.credentialsSchema)())
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, SelectFieldChoice, validate, ValidationResult } from './minified-utils/fields';
7
+ export { 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';
@@ -23,11 +23,32 @@ class Context {
23
23
  else {
24
24
  return source;
25
25
  }
26
- if (!unresolved.includes(`{{`)) {
26
+ if (!unresolved || !unresolved.includes(`{{`)) {
27
27
  return source;
28
28
  }
29
29
  const template = handlebars_1.default.compile(unresolved);
30
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
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ return this.resolve(isObject ? JSON.parse(unresolved) : unresolved, clonedData);
51
+ }
31
52
  return isObject ? JSON.parse(resolved) : resolved;
32
53
  };
33
54
  handlebars_1.default.registerHelper({
@@ -22,8 +22,10 @@ type Common = {
22
22
  array_length?: number;
23
23
  });
24
24
  export type Field<Extend = Record<never, never>> = Common & InputField<Extend>;
25
- type InputField<Extend = Record<never, never>> = (AnyInputField<Extend> | StringInputField<Extend> | NumberInputField<Extend> | BooleanInputField<Extend> | DateTimeInputField<Extend> | DateInputField<Extend> | TimeInputField<Extend> | DictInputField<Extend>) & Extend;
26
- type Any = string | number | boolean;
25
+ type InputField<Extend = Record<never, never>> = (AnyInputField<Extend> | StringInputField<Extend> | NumberInputField<Extend> | BooleanInputField<Extend> | DateTimeInputField<Extend> | DateInputField<Extend> | TimeInputField<Extend> | DictInputField<Extend> | FileInputField<Extend>) & Extend;
26
+ type Any = string | number | boolean | Any[] | {
27
+ [key: string]: Any;
28
+ };
27
29
  type AnyInputField<Extend = Record<never, never>> = {
28
30
  type: `any`;
29
31
  default?: Any | Any[];
@@ -36,7 +38,8 @@ type StringInputField<Extend = Record<never, never>> = {
36
38
  max?: number;
37
39
  regexp?: string;
38
40
  pattern?: `email` | `uri`;
39
- } & (SelectField<string, Extend> | FileField | TextareaField | RichTextField | {});
41
+ escape?: boolean;
42
+ } & (SelectField<string, Extend> | TextareaField | RichTextField | {});
40
43
  type NumberInputField<Extend = Record<never, never>> = {
41
44
  type: `number`;
42
45
  default?: number | number[];
@@ -80,18 +83,25 @@ type DictInputField<Extend = Record<never, never>> = {
80
83
  items?: Field<Extend>[];
81
84
  unknown?: boolean;
82
85
  };
83
- type SelectField<Values = string | number | boolean, Extend = Record<never, never>> = {
86
+ export type File = {
87
+ name: string;
88
+ type: string;
89
+ content: string;
90
+ };
91
+ type FileInputField<Extend = Record<never, never>> = {
92
+ type: `file`;
93
+ default?: File | File[];
94
+ accept?: string | string[];
95
+ } & (SelectField<File, Extend> | {});
96
+ type SelectField<Values = string | number | boolean | File, Extend = Record<never, never>> = {
84
97
  format: `select`;
85
98
  choices: SelectFieldChoice<Values, Extend>[] | string;
86
99
  unknown?: boolean;
87
100
  };
88
- export type SelectFieldChoice<Values = string | number | boolean, Extend = Record<never, never>> = {
101
+ export type SelectFieldChoice<Values = string | number | boolean | File, Extend = Record<never, never>> = {
89
102
  key: string;
90
103
  value: Values;
91
104
  } & Extend;
92
- type FileField = {
93
- format: `file`;
94
- };
95
105
  type TextareaField = {
96
106
  format: `textarea`;
97
107
  };
@@ -6,10 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.extract = exports.validate = exports.fieldsSchema = exports.fieldSchema = void 0;
7
7
  const joi_1 = __importDefault(require("joi"));
8
8
  const lodash_1 = __importDefault(require("lodash"));
9
- const where_1 = require("./where");
10
9
  const moment_timezone_1 = __importDefault(require("moment-timezone"));
11
- const axios_1 = __importDefault(require("axios"));
12
10
  const context_1 = __importDefault(require("./context"));
11
+ const where_1 = require("./where");
13
12
  const conditionSchema = () => joi_1.default.object({
14
13
  condition: joi_1.default.object(),
15
14
  overrides: joi_1.default.object(),
@@ -38,7 +37,7 @@ const commonFieldSchema = () => joi_1.default.object({
38
37
  otherwise: joi_1.default.forbidden(),
39
38
  }),
40
39
  });
41
- const types = [`any`, `string`, `number`, `boolean`, `date`, `datetime`, `time`, `dict`];
40
+ const types = [`any`, `string`, `number`, `boolean`, `date`, `datetime`, `time`, `dict`, `file`];
42
41
  const inputFieldBaseSchema = () => joi_1.default.object({
43
42
  type: joi_1.default.string().required()
44
43
  .valid(...types),
@@ -56,7 +55,8 @@ const defaultSchema = (type) => joi_1.default.when(`as_array`, {
56
55
  then: joi_1.default.array().items(type),
57
56
  otherwise: type,
58
57
  });
59
- const anySchema = () => joi_1.default.alternatives(joi_1.default.string(), joi_1.default.bool(), joi_1.default.number());
58
+ const anySchema = () => joi_1.default.alternatives(joi_1.default.string(), joi_1.default.bool(), joi_1.default.number(), joi_1.default.array().items(joi_1.default.link(`#any`)), joi_1.default.object().pattern(joi_1.default.string(), joi_1.default.link(`#any`)))
59
+ .id(`any`);
60
60
  const anyInputFieldSchema = () => joi_1.default.object({
61
61
  default: defaultSchema(anySchema()),
62
62
  })
@@ -77,7 +77,7 @@ const stringInputFieldSchema = () => joi_1.default.object({
77
77
  pattern: joi_1.default.string().valid(`email`, `uri`),
78
78
  })
79
79
  .concat(joi_1.default.object({
80
- format: joi_1.default.string().valid(`file`, `select`, `textarea`, `rich-text`),
80
+ format: joi_1.default.string().valid(`select`, `textarea`, `rich-text`),
81
81
  choices: joi_1.default.when(`format`, {
82
82
  is: `select`,
83
83
  then: choicesSchema(joi_1.default.string()).required(),
@@ -147,6 +147,23 @@ const dictInputFieldSchema = () => joi_1.default.object({
147
147
  otherwise: joi_1.default.forbidden(),
148
148
  }),
149
149
  });
150
+ const fileSchema = () => joi_1.default.object({
151
+ name: joi_1.default.string().required(),
152
+ type: joi_1.default.string().required(),
153
+ content: joi_1.default.string().required(),
154
+ });
155
+ const fileInputFieldSchema = () => joi_1.default.object({
156
+ default: defaultSchema(fileSchema()),
157
+ accept: joi_1.default.alternatives(joi_1.default.string(), joi_1.default.array().items(joi_1.default.string())).optional(),
158
+ })
159
+ .concat(joi_1.default.object({
160
+ format: joi_1.default.string().valid(`select`),
161
+ choices: joi_1.default.when(`format`, {
162
+ is: `select`,
163
+ then: choicesSchema(fileSchema()).required(),
164
+ otherwise: joi_1.default.forbidden(),
165
+ }),
166
+ }));
150
167
  const fieldSchema = () => inputFieldBaseSchema()
151
168
  .id(`field`)
152
169
  .when(joi_1.default.object({ type: joi_1.default.valid(`any`) }).unknown(), {
@@ -176,6 +193,10 @@ const fieldSchema = () => inputFieldBaseSchema()
176
193
  .when(joi_1.default.object({ type: joi_1.default.valid(`dict`) }).unknown(), {
177
194
  then: inputFieldBaseSchema()
178
195
  .concat(dictInputFieldSchema()),
196
+ })
197
+ .when(joi_1.default.object({ type: joi_1.default.valid(`file`) }).unknown(), {
198
+ then: inputFieldBaseSchema()
199
+ .concat(fileInputFieldSchema()),
179
200
  })
180
201
  .when(joi_1.default.object({ type: joi_1.default.invalid(...types) }).unknown(), {
181
202
  then: joi_1.default.forbidden(),
@@ -215,47 +236,6 @@ const getFieldSchema = (field) => {
215
236
  if (field.pattern === `uri`) {
216
237
  typeSchema = typeSchema.uri();
217
238
  }
218
- if (`format` in field && field.format === `file`) {
219
- typeSchema = typeSchema.external(async (value, helpers) => {
220
- if (!helpers.original && !field.required) {
221
- return helpers.original;
222
- }
223
- const [asURL, asBase64] = await Promise.allSettled([
224
- (async () => {
225
- try {
226
- const { value: url, error } = joi_1.default.string().uri().validate(helpers.original);
227
- if (error) {
228
- throw new Error(`Invalid URL schema`);
229
- }
230
- const { data: content } = await axios_1.default.get(url, { responseType: `arraybuffer` });
231
- return Buffer.from(content).toString(`base64`);
232
- }
233
- catch (e) {
234
- throw e;
235
- }
236
- })(),
237
- (async () => {
238
- try {
239
- const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
240
- if (!base64regex.test(helpers.original)) {
241
- throw new Error(`Invalid regexp`);
242
- }
243
- return Buffer.from(helpers.original, `base64`).toString(`base64`);
244
- }
245
- catch (e) {
246
- throw e;
247
- }
248
- })(),
249
- ]);
250
- if (asURL.status === `fulfilled`) {
251
- return asURL.value;
252
- }
253
- else if (asBase64.status === `fulfilled`) {
254
- return asBase64.value;
255
- }
256
- return helpers.message({ external: `invalid value, expected URL or Base64 string` });
257
- });
258
- }
259
239
  break;
260
240
  case `date`:
261
241
  case `datetime`:
@@ -305,12 +285,37 @@ const getFieldSchema = (field) => {
305
285
  }
306
286
  break;
307
287
  case `dict`:
288
+ let objSchema;
308
289
  if (field.items) {
309
- typeSchema = getFieldsSchema(field.items).unknown(field.unknown || false);
290
+ objSchema = getFieldsSchema(field.items).unknown(field.unknown || false);
310
291
  }
311
292
  else {
312
- typeSchema = joi_1.default.object().unknown(true);
293
+ objSchema = joi_1.default.object().unknown(true);
313
294
  }
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
+ }));
306
+ break;
307
+ 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
+ }));
314
319
  break;
315
320
  }
316
321
  if (`format` in field && field.format === `select`) {
@@ -323,16 +328,27 @@ const getFieldSchema = (field) => {
323
328
  }
324
329
  }
325
330
  if (field.as_array) {
326
- typeSchema = joi_1.default.array().items(typeSchema);
331
+ let arraySchema = joi_1.default.array().items(typeSchema);
327
332
  if (field.array_length) {
328
- typeSchema = typeSchema.length(field.array_length);
333
+ arraySchema = arraySchema.length(field.array_length);
329
334
  }
330
335
  if (field.array_min) {
331
- typeSchema = typeSchema.min(field.array_min);
336
+ arraySchema = arraySchema.min(field.array_min);
332
337
  }
333
338
  if (field.array_max) {
334
- typeSchema = typeSchema.max(field.array_max);
339
+ arraySchema = arraySchema.max(field.array_max);
335
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
+ }));
336
352
  }
337
353
  if (field.disabled) {
338
354
  typeSchema = typeSchema.forbidden();
@@ -1,8 +1,8 @@
1
1
  import { Provider } from "../entities/provider";
2
2
  export declare class Protocol {
3
3
  private readonly provider;
4
- private readonly path;
5
- constructor(provider: Provider, path: string);
4
+ private readonly configuration;
5
+ constructor(provider: Provider, configuration: Protocol.Configuration);
6
6
  evaluate: (req: Protocol.Command) => Promise<any>;
7
7
  }
8
8
  export declare namespace Protocol {
@@ -16,6 +16,8 @@ export declare namespace Protocol {
16
16
  configuration: Record<string, any>;
17
17
  };
18
18
  context: {
19
+ organization: string;
20
+ workspace: number;
19
21
  environment: Record<string, any>;
20
22
  localization: {
21
23
  locale: string;
@@ -39,4 +41,12 @@ export declare namespace Protocol {
39
41
  command: `list:locales`;
40
42
  };
41
43
  type Command = Invoke | CredentialList | ActionList | TriggerList | SourceList | LocaleList;
44
+ type SourcesConfiguration = {
45
+ host: string;
46
+ token: string;
47
+ };
48
+ type Configuration = {
49
+ path: string;
50
+ sources: SourcesConfiguration;
51
+ };
42
52
  }
@@ -4,8 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Protocol = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
7
8
  const joi_1 = __importDefault(require("joi"));
8
9
  const lodash_1 = __importDefault(require("lodash"));
10
+ const moment_timezone_1 = __importDefault(require("moment-timezone"));
9
11
  const core_1 = require("../core");
10
12
  const provider_1 = require("../entities/provider");
11
13
  const locales_1 = require("./locales");
@@ -41,8 +43,87 @@ const localeListSchema = joi_1.default.object({
41
43
  command: joi_1.default.string().valid(`list:locales`).required(),
42
44
  }).required();
43
45
  const commandSchema = joi_1.default.alternatives(invokeSchema, credentialListSchema, actionListSchema, triggerListSchema, sourceListSchema, localeListSchema);
46
+ const API_SOURCES = (provider, configuration, context) => ({
47
+ register: async (source, data) => {
48
+ try {
49
+ const { data: [created] } = await axios_1.default.post(`/api/v1/sources`, {
50
+ organization: context.organization,
51
+ workspace: context.workspace,
52
+ provider: provider.id,
53
+ version: provider.version,
54
+ source: source,
55
+ data: data,
56
+ created_date: (0, moment_timezone_1.default)().format(`YYYY-MM-DDTHH:mm:ssZ`),
57
+ }, {
58
+ baseURL: configuration.host,
59
+ headers: {
60
+ "X-Internal-Token": configuration.token,
61
+ },
62
+ });
63
+ if (!created || !created.uuid) {
64
+ throw new Error(`Source registration failed`);
65
+ }
66
+ return { id: created.uuid };
67
+ }
68
+ catch (e) {
69
+ throw e;
70
+ }
71
+ },
72
+ unregister: async (source, id) => {
73
+ try {
74
+ const { data: [get] } = await axios_1.default.post(`/api/v1/sources/list`, {
75
+ organization: context.organization,
76
+ workspace: context.workspace,
77
+ provider: provider.id,
78
+ version: provider.version,
79
+ source: source,
80
+ uuid: id,
81
+ }, {
82
+ baseURL: configuration.host,
83
+ headers: {
84
+ "X-Internal-Token": configuration.token,
85
+ },
86
+ });
87
+ if (!get) {
88
+ throw new Error(`Source with ID ${id} not found`);
89
+ }
90
+ await axios_1.default.delete(`/api/v1/sources/${get.id}`, {
91
+ baseURL: configuration.host,
92
+ headers: {
93
+ "X-Internal-Token": configuration.token,
94
+ },
95
+ });
96
+ }
97
+ catch (e) {
98
+ throw e;
99
+ }
100
+ },
101
+ consume: async (source) => {
102
+ try {
103
+ const { data: sources } = await axios_1.default.post(`/api/v1/sources/list`, {
104
+ organization: context.organization,
105
+ workspace: context.workspace,
106
+ provider: provider.id,
107
+ version: provider.version,
108
+ source: source,
109
+ }, {
110
+ baseURL: configuration.host,
111
+ headers: {
112
+ "X-Internal-Token": configuration.token,
113
+ },
114
+ });
115
+ return sources.map((s) => ({
116
+ id: s.uuid,
117
+ data: s.data,
118
+ }));
119
+ }
120
+ catch (e) {
121
+ throw e;
122
+ }
123
+ },
124
+ });
44
125
  class Protocol {
45
- constructor(provider, path) {
126
+ constructor(provider, configuration) {
46
127
  this.evaluate = async (req) => {
47
128
  try {
48
129
  const validation = commandSchema.validate(req);
@@ -69,7 +150,7 @@ class Protocol {
69
150
  response = Object.values(this.provider.sources || {});
70
151
  break;
71
152
  case `list:locales`:
72
- const i18nInstance = (0, locales_1.i18n)({ directory: `${this.path}/locales` });
153
+ const i18nInstance = (0, locales_1.i18n)({ directory: `${this.configuration.path}/locales` });
73
154
  response = i18nInstance.i18n.getLocales().map((l) => {
74
155
  i18nInstance.i18n.setLocale(l);
75
156
  return {
@@ -90,7 +171,10 @@ class Protocol {
90
171
  const invokeInstance = (0, core_1.core)({
91
172
  env: req.context.environment,
92
173
  localization: req.context.localization,
93
- sources: null,
174
+ sources: API_SOURCES(this.provider, this.configuration.sources, {
175
+ organization: req.context.organization,
176
+ workspace: req.context.workspace,
177
+ }),
94
178
  });
95
179
  response = await func(invokeInstance, req.params.configuration);
96
180
  break;
@@ -108,7 +192,7 @@ class Protocol {
108
192
  throw new Error(`Invalid provider for protocol`);
109
193
  }
110
194
  this.provider = provider;
111
- this.path = path;
195
+ this.configuration = configuration;
112
196
  }
113
197
  }
114
198
  exports.Protocol = Protocol;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beseif-solutions/prow-core",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "description": "Prow core functionalities and classes",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/core.ts CHANGED
@@ -1,13 +1,15 @@
1
- import axios from "axios";
1
+ import axios, { AxiosStatic } from "axios";
2
2
  import Joi from "joi";
3
- import _ from "lodash";
3
+ import _, { LoDashStatic } from "lodash";
4
4
  import moment from "moment-timezone";
5
5
  import { FromCatalog, FromDirectory, i18n, T } from "./minified-utils/locales";
6
6
 
7
- type SourceFunctions = {
8
- register: <T>(source: string, data: T) => Promise<{ id: string }>,
9
- consume: <T>(source: string) => Promise<{ id: string, data: T }[]>,
10
- unregister: (source: string, id: string) => Promise<void>,
7
+ type SyncOrAsync<T> = T | Promise<T>;
8
+
9
+ export type SourceFunctions = {
10
+ register: <T>(source: string, data: T) => SyncOrAsync<{ id: string }>,
11
+ consume: <T>(source: string) => SyncOrAsync<{ id: string, data: T }[]>,
12
+ unregister: (source: string, id: string) => SyncOrAsync<void>,
11
13
  };
12
14
 
13
15
  const FALLBACK_SOURCES: SourceFunctions = {
@@ -23,11 +25,11 @@ const FALLBACK_SOURCES: SourceFunctions = {
23
25
  };
24
26
 
25
27
  export type Core = {
26
- axios: typeof axios,
28
+ axios: AxiosStatic,
27
29
  moment: typeof moment,
28
- lodash: typeof _,
30
+ lodash: LoDashStatic,
29
31
  env: Record<string, any>,
30
- joi: typeof Joi,
32
+ joi: Joi.Root,
31
33
  sources?: SourceFunctions,
32
34
  t: T,
33
35
  };
@@ -74,6 +74,11 @@ export type ImmediateTrigger<Config extends EventConfig = {}> = CommonEvents<Con
74
74
  inputs: { fields: Record<string, any>, link: string, setup?: any },
75
75
  outputs: { done: boolean },
76
76
  }>,
77
+ challenge?: CoreFunction<{
78
+ credentials: Config[`credentials`],
79
+ inputs: { fields: Record<string, any>, link: string, setup?: any },
80
+ outputs: { done: boolean, response?: any },
81
+ }>,
77
82
  handle: CoreFunction<{
78
83
  credentials: Config[`credentials`],
79
84
  inputs: { fields: Record<string, any>, request: Request, setup?: any },
@@ -201,9 +206,10 @@ export const immediateTriggerSchema = () =>
201
206
  functions: Joi.object({
202
207
  mount: Joi.function(),
203
208
  unmount: Joi.function(),
204
- fire: Joi.function(),
209
+ challenge: Joi.function(),
205
210
  handle: Joi.function().required(),
206
211
  poll: Joi.function(),
212
+ fire: Joi.function(),
207
213
  }).required(),
208
214
  })
209
215
  .concat(commonSchema())
@@ -1,11 +1,12 @@
1
1
  import Joi from "joi";
2
+ import { idSchema } from "./commons";
2
3
  import { Credentials, credentialsSchema } from "./credentials";
3
4
  import { Action, actionSchema, Trigger, triggerSchema } from "./events";
4
- import { idSchema } from "./commons";
5
5
  import { Source, sourceSchema } from "./source";
6
6
 
7
7
  export type Provider = {
8
8
  id: string,
9
+ version: string,
9
10
  model: `provider`,
10
11
  credentials: Record<string, Credentials>,
11
12
  triggers: Record<string, Trigger>,
@@ -16,6 +17,7 @@ export type Provider = {
16
17
  export const providerSchema = () =>
17
18
  Joi.object({
18
19
  id: idSchema(),
20
+ version: Joi.string().required(),
19
21
  model: Joi.valid(`provider`).required(),
20
22
  credentials: Joi.object()
21
23
  .pattern(idSchema(), credentialsSchema())
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, SelectFieldChoice, validate, ValidationResult } from './minified-utils/fields';
7
+ export { 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';
@@ -43,10 +43,33 @@ class Context {
43
43
  unresolved = source;
44
44
  } else { return source; }
45
45
 
46
- if (!unresolved.includes(`{{`)) { return source; }
46
+ if (!unresolved || !unresolved.includes(`{{`)) { return source; }
47
47
 
48
48
  const template = Handlebars.compile(unresolved);
49
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
+ }
65
+
66
+ }
67
+ }
68
+ }
69
+
70
+ return this.resolve(isObject ? JSON.parse(unresolved) : unresolved, clonedData);
71
+ }
72
+
50
73
  return isObject ? JSON.parse(resolved) : resolved;
51
74
  };
52
75
 
@@ -1,10 +1,9 @@
1
- import Joi, { ArraySchema, DateSchema, NumberSchema, StringSchema } from "joi";
1
+ import Joi, { DateSchema, NumberSchema, StringSchema } from "joi";
2
2
  import _ from "lodash";
3
- import { OneOf } from "./types";
4
- import { where, Where } from "./where";
5
3
  import moment from "moment-timezone";
6
- import axios from "axios";
7
4
  import context from "./context";
5
+ import { OneOf } from "./types";
6
+ import { where, Where } from "./where";
8
7
 
9
8
  export type Condition = {
10
9
  condition: Where,
@@ -39,9 +38,10 @@ type InputField<Extend = Record<never, never>> = (
39
38
  | DateInputField<Extend>
40
39
  | TimeInputField<Extend>
41
40
  | DictInputField<Extend>
41
+ | FileInputField<Extend>
42
42
  ) & Extend;
43
43
 
44
- type Any = string | number | boolean;
44
+ type Any = string | number | boolean | Any[] | { [key: string]: Any };
45
45
 
46
46
  type AnyInputField<Extend = Record<never, never>> = {
47
47
  type: `any`,
@@ -59,9 +59,9 @@ type StringInputField<Extend = Record<never, never>> = {
59
59
  max?: number,
60
60
  regexp?: string,
61
61
  pattern?: `email` | `uri`,
62
+ escape?: boolean,
62
63
  } & (
63
64
  | SelectField<string, Extend>
64
- | FileField
65
65
  | TextareaField
66
66
  | RichTextField
67
67
  | {}
@@ -127,24 +127,34 @@ type DictInputField<Extend = Record<never, never>> = {
127
127
  unknown?: boolean,
128
128
  };
129
129
 
130
+ export type File = {
131
+ name: string,
132
+ type: string,
133
+ content: string, // base64 encoded content
134
+ };
135
+
136
+ type FileInputField<Extend = Record<never, never>> = {
137
+ type: `file`,
138
+ default?: File | File[],
139
+ accept?: string | string[], // mime type
140
+ } & (
141
+ | SelectField<File, Extend>
142
+ | {}
143
+ );
144
+
130
145
  /* select configuration */
131
- type SelectField<Values = string | number | boolean, Extend = Record<never, never>> = {
146
+ type SelectField<Values = string | number | boolean | File, Extend = Record<never, never>> = {
132
147
  format: `select`,
133
148
  // el type string representa el nombre de una función del repositorio que devuelve SelectFieldChoice[]
134
149
  choices: SelectFieldChoice<Values, Extend>[] | string,
135
150
  unknown?: boolean,
136
151
  };
137
152
 
138
- export type SelectFieldChoice<Values = string | number | boolean, Extend = Record<never, never>> = {
153
+ export type SelectFieldChoice<Values = string | number | boolean | File, Extend = Record<never, never>> = {
139
154
  key: string,
140
155
  value: Values,
141
156
  } & Extend;
142
157
 
143
- /* file configuration */
144
- type FileField = {
145
- format: `file`,
146
- };
147
-
148
158
  /* textarea configuration */
149
159
  type TextareaField = {
150
160
  format: `textarea`,
@@ -201,7 +211,7 @@ const commonFieldSchema = () =>
201
211
  }),
202
212
  });
203
213
 
204
- const types = [`any`, `string`, `number`, `boolean`, `date`, `datetime`, `time`, `dict`];
214
+ const types = [`any`, `string`, `number`, `boolean`, `date`, `datetime`, `time`, `dict`, `file`];
205
215
 
206
216
  const inputFieldBaseSchema = () =>
207
217
  Joi.object({
@@ -233,7 +243,13 @@ const defaultSchema = (type: Joi.Schema) =>
233
243
  });
234
244
 
235
245
  const anySchema = () =>
236
- Joi.alternatives(Joi.string(), Joi.bool(), Joi.number());
246
+ Joi.alternatives(
247
+ Joi.string(),
248
+ Joi.bool(),
249
+ Joi.number(),
250
+ Joi.array().items(Joi.link(`#any`)),
251
+ Joi.object().pattern(Joi.string(), Joi.link(`#any`)))
252
+ .id(`any`);
237
253
 
238
254
  const anyInputFieldSchema = () =>
239
255
  Joi.object({
@@ -259,7 +275,7 @@ const stringInputFieldSchema = () =>
259
275
  })
260
276
  .concat(
261
277
  Joi.object({
262
- format: Joi.string().valid(`file`, `select`, `textarea`, `rich-text`),
278
+ format: Joi.string().valid(`select`, `textarea`, `rich-text`),
263
279
  choices: Joi.when(`format`, {
264
280
  is: `select`,
265
281
  then: choicesSchema(Joi.string()).required(),
@@ -341,6 +357,31 @@ const dictInputFieldSchema = () =>
341
357
  }),
342
358
  });
343
359
 
360
+ const fileSchema = () => Joi.object({
361
+ name: Joi.string().required(),
362
+ type: Joi.string().required(), // mime type
363
+ content: Joi.string().required(), // base64 encoded content
364
+ });
365
+
366
+ const fileInputFieldSchema = () =>
367
+ Joi.object({
368
+ default: defaultSchema(fileSchema()),
369
+ accept: Joi.alternatives(
370
+ Joi.string(),
371
+ Joi.array().items(Joi.string())
372
+ ).optional(),
373
+ })
374
+ .concat(
375
+ Joi.object({
376
+ format: Joi.string().valid(`select`),
377
+ choices: Joi.when(`format`, {
378
+ is: `select`,
379
+ then: choicesSchema(fileSchema()).required(),
380
+ otherwise: Joi.forbidden(),
381
+ }),
382
+ })
383
+ );
384
+
344
385
  export const fieldSchema = () =>
345
386
  inputFieldBaseSchema()
346
387
  .id(`field`)
@@ -372,6 +413,11 @@ export const fieldSchema = () =>
372
413
  then: inputFieldBaseSchema()
373
414
  .concat(dictInputFieldSchema()),
374
415
  })
416
+ .when(Joi.object({ type: Joi.valid(`file`) }).unknown(), {
417
+ then: inputFieldBaseSchema()
418
+ .concat(fileInputFieldSchema()),
419
+ })
420
+ // invalid fallback
375
421
  .when(Joi.object({ type: Joi.invalid(...types) }).unknown(), {
376
422
  then: Joi.forbidden(),
377
423
  });
@@ -401,39 +447,6 @@ const getFieldSchema = (field: Field) => {
401
447
  if (field.pattern === `email`) { typeSchema = (typeSchema as StringSchema).email(); }
402
448
  if (field.pattern === `uri`) { typeSchema = (typeSchema as StringSchema).uri(); }
403
449
 
404
- if (`format` in field && field.format === `file`) {
405
- typeSchema = typeSchema.external(async (value, helpers) => {
406
- if (!helpers.original && !field.required) { return helpers.original; }
407
-
408
- const [asURL, asBase64] = await Promise.allSettled([
409
- (async () => {
410
- try {
411
- const { value: url, error } = Joi.string().uri().validate(helpers.original);
412
- if (error) { throw new Error(`Invalid URL schema`); }
413
-
414
- const { data: content } = await axios.get(url, { responseType: `arraybuffer` });
415
- return Buffer.from(content).toString(`base64`);
416
- } catch (e) { throw e; }
417
- })(),
418
- (async () => {
419
- try {
420
- // fastest approach: https://stackoverflow.com/a/8571649
421
- const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
422
- if (!base64regex.test(helpers.original)) { throw new Error(`Invalid regexp`); }
423
- return Buffer.from(helpers.original, `base64`).toString(`base64`);
424
- } catch (e) { throw e; }
425
- })(),
426
- ]);
427
-
428
- if (asURL.status === `fulfilled`) {
429
- return asURL.value;
430
- } else if (asBase64.status === `fulfilled`) {
431
- return asBase64.value;
432
- }
433
-
434
- return helpers.message({ external: `invalid value, expected URL or Base64 string` });
435
- });
436
- }
437
450
  break;
438
451
  case `date`:
439
452
  case `datetime`:
@@ -473,11 +486,43 @@ const getFieldSchema = (field: Field) => {
473
486
  }
474
487
  break;
475
488
  case `dict`:
489
+ let objSchema: Joi.ObjectSchema;
476
490
  if (field.items) {
477
- typeSchema = getFieldsSchema(field.items).unknown(field.unknown || false);
491
+ objSchema = getFieldsSchema(field.items).unknown(field.unknown || false);
478
492
  } else {
479
- typeSchema = Joi.object().unknown(true);
493
+ objSchema = Joi.object().unknown(true);
480
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
+
510
+ break;
511
+ 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
+
481
526
  break;
482
527
  }
483
528
 
@@ -492,11 +537,25 @@ const getFieldSchema = (field: Field) => {
492
537
  }
493
538
 
494
539
  if (field.as_array) {
495
- typeSchema = Joi.array().items(typeSchema);
540
+ let arraySchema = Joi.array().items(typeSchema);
496
541
 
497
- if (field.array_length) { typeSchema = (typeSchema as ArraySchema).length(field.array_length); }
498
- if (field.array_min) { typeSchema = (typeSchema as ArraySchema).min(field.array_min); }
499
- if (field.array_max) { typeSchema = (typeSchema as ArraySchema).max(field.array_max); }
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; }
551
+
552
+ const parsed = JSON.parse(value);
553
+ const { value: arrValue, error } = arraySchema.validate(parsed, { abortEarly: false });
554
+ if (error) { throw error; }
555
+
556
+ return arrValue;
557
+ })
558
+ );
500
559
  }
501
560
 
502
561
  if (field.disabled) { typeSchema = typeSchema.forbidden(); }
@@ -1,6 +1,8 @@
1
+ import axios from "axios";
1
2
  import Joi from "joi";
2
3
  import _ from "lodash";
3
- import { core } from "../core";
4
+ import moment from "moment-timezone";
5
+ import { core, SourceFunctions } from "../core";
4
6
  import { Credentials } from "../entities/credentials";
5
7
  import { Action, Trigger } from "../entities/events";
6
8
  import { Provider, providerSchema } from "../entities/provider";
@@ -52,16 +54,84 @@ const commandSchema = Joi.alternatives(
52
54
  localeListSchema,
53
55
  );
54
56
 
57
+ const API_SOURCES = (provider: Provider, configuration: Protocol.SourcesConfiguration, context: { organization: string, workspace: number }): SourceFunctions => ({
58
+ register: async (source, data) => {
59
+ try {
60
+ const { data: [created] } = await axios.post(`/api/v1/sources`, {
61
+ organization: context.organization,
62
+ workspace: context.workspace,
63
+ provider: provider.id,
64
+ version: provider.version,
65
+ source: source,
66
+ data: data,
67
+ created_date: moment().format(`YYYY-MM-DDTHH:mm:ssZ`),
68
+ }, {
69
+ baseURL: configuration.host,
70
+ headers: {
71
+ "X-Internal-Token": configuration.token,
72
+ },
73
+ });
74
+ if (!created || !created.uuid) { throw new Error(`Source registration failed`); }
75
+ return { id: created.uuid };
76
+ } catch (e) { throw e; }
77
+ },
78
+ unregister: async (source, id) => {
79
+ try {
80
+ const { data: [get] } = await axios.post(`/api/v1/sources/list`, {
81
+ organization: context.organization,
82
+ workspace: context.workspace,
83
+ provider: provider.id,
84
+ version: provider.version,
85
+ source: source,
86
+ uuid: id,
87
+ }, {
88
+ baseURL: configuration.host,
89
+ headers: {
90
+ "X-Internal-Token": configuration.token,
91
+ },
92
+ });
93
+ if (!get) { throw new Error(`Source with ID ${id} not found`); }
94
+
95
+ await axios.delete(`/api/v1/sources/${get.id}`, {
96
+ baseURL: configuration.host,
97
+ headers: {
98
+ "X-Internal-Token": configuration.token,
99
+ },
100
+ });
101
+ } catch (e) { throw e; }
102
+ },
103
+ consume: async (source) => {
104
+ try {
105
+ const { data: sources } = await axios.post(`/api/v1/sources/list`, {
106
+ organization: context.organization,
107
+ workspace: context.workspace,
108
+ provider: provider.id,
109
+ version: provider.version,
110
+ source: source,
111
+ }, {
112
+ baseURL: configuration.host,
113
+ headers: {
114
+ "X-Internal-Token": configuration.token,
115
+ },
116
+ });
117
+ return sources.map((s) => ({
118
+ id: s.uuid,
119
+ data: s.data,
120
+ }));
121
+ } catch (e) { throw e; }
122
+ },
123
+ });
124
+
55
125
  export class Protocol {
56
126
 
57
127
  private readonly provider: Provider;
58
- private readonly path: string;
128
+ private readonly configuration: Protocol.Configuration;
59
129
 
60
- constructor(provider: Provider, path: string) {
130
+ constructor(provider: Provider, configuration: Protocol.Configuration) {
61
131
  const providerValidation = providerSchema().validate(provider);
62
132
  if (providerValidation.error) { throw new Error(`Invalid provider for protocol`); }
63
133
  this.provider = provider;
64
- this.path = path;
134
+ this.configuration = configuration;
65
135
  }
66
136
 
67
137
  public evaluate = async (req: Protocol.Command): Promise<any> => {
@@ -90,7 +160,7 @@ export class Protocol {
90
160
  response = Object.values(this.provider.sources || {});
91
161
  break;
92
162
  case `list:locales`:
93
- const i18nInstance = i18n({ directory: `${this.path}/locales` });
163
+ const i18nInstance = i18n({ directory: `${this.configuration.path}/locales` });
94
164
  response = i18nInstance.i18n.getLocales().map((l) => {
95
165
  i18nInstance.i18n.setLocale(l);
96
166
  return {
@@ -109,8 +179,10 @@ export class Protocol {
109
179
  const invokeInstance = core({
110
180
  env: req.context.environment,
111
181
  localization: req.context.localization,
112
- // TODO: sources
113
- sources: null,
182
+ sources: API_SOURCES(this.provider, this.configuration.sources, {
183
+ organization: req.context.organization,
184
+ workspace: req.context.workspace,
185
+ }),
114
186
  });
115
187
 
116
188
  response = await func(invokeInstance, req.params.configuration);
@@ -137,6 +209,8 @@ export namespace Protocol {
137
209
  configuration: Record<string, any>,
138
210
  },
139
211
  context: {
212
+ organization: string,
213
+ workspace: number,
140
214
  environment: Record<string, any>,
141
215
  localization: {
142
216
  locale: string,
@@ -166,4 +240,14 @@ export namespace Protocol {
166
240
  };
167
241
 
168
242
  export type Command = Invoke | CredentialList | ActionList | TriggerList | SourceList | LocaleList;
243
+
244
+ export type SourcesConfiguration = {
245
+ host: string,
246
+ token: string,
247
+ };
248
+
249
+ export type Configuration = {
250
+ path: string,
251
+ sources: SourcesConfiguration,
252
+ };
169
253
  }