@friggframework/core 2.0.0-next.0 → 2.0.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,41 +1,122 @@
1
1
  const { IntegrationMapping } = require('./integration-mapping');
2
+ const { Options } = require('./options');
3
+ const constantsToBeMigrated = {
4
+ defaultEvents: {
5
+ ON_CREATE: 'ON_CREATE',
6
+ ON_UPDATE: 'ON_UPDATE',
7
+ ON_DELETE: 'ON_DELETE',
8
+ GET_CONFIG_OPTIONS: 'GET_CONFIG_OPTIONS',
9
+ REFRESH_CONFIG_OPTIONS: 'REFRESH_CONFIG_OPTIONS',
10
+ GET_USER_ACTIONS: 'GET_USER_ACTIONS',
11
+ GET_USER_ACTION_OPTIONS: 'GET_USER_ACTION_OPTIONS',
12
+ REFRESH_USER_ACTION_OPTIONS: 'REFRESH_USER_ACTION_OPTIONS',
13
+ // etc...
14
+ },
15
+ types: {
16
+ LIFE_CYCLE_EVENT: 'LIFE_CYCLE_EVENT',
17
+ USER_ACTION: 'USER_ACTION',
18
+ },
19
+ };
2
20
 
3
21
  class IntegrationBase {
22
+ static getOptionDetails() {
23
+ const options = new Options({
24
+ module: Object.values(this.Definition.modules)[0], // This is a placeholder until we revamp the frontend
25
+ ...this.Definition,
26
+ });
27
+ return options.get();
28
+ }
4
29
  /**
5
- * CHILDREN SHOULD SPECIFY A CONFIG
30
+ * CHILDREN SHOULD SPECIFY A DEFINITION FOR THE INTEGRATION
6
31
  */
7
- static Config = {
32
+ static Definition = {
8
33
  name: 'Integration Name',
9
34
  version: '0.0.0', // Integration Version, used for migration and storage purposes, as well as display
10
35
  supportedVersions: [], // Eventually usable for deprecation and future test version purposes
11
36
 
12
- // an array of events that are process(able) by this Integration
13
- events: [],
37
+ modules: {},
38
+ display: {
39
+ name: 'Integration Name',
40
+ logo: '',
41
+ description: '',
42
+ // etc...
43
+ },
14
44
  };
15
45
 
16
46
  static getName() {
17
- return this.Config.name;
47
+ return this.Definition.name;
18
48
  }
19
49
 
20
50
  static getCurrentVersion() {
21
- return this.Config.version;
51
+ return this.Definition.version;
52
+ }
53
+ loadModules() {
54
+ // Load all the modules defined in Definition.modules
55
+ const moduleNames = Object.keys(this.constructor.Definition.modules);
56
+ for (const moduleName of moduleNames) {
57
+ const { definition } =
58
+ this.constructor.Definition.modules[moduleName];
59
+ if (typeof definition.API === 'function') {
60
+ this[moduleName] = { api: new definition.API() };
61
+ } else {
62
+ throw new Error(
63
+ `Module ${moduleName} must be a function that extends IntegrationModule`
64
+ );
65
+ }
66
+ }
67
+ }
68
+ registerEventHandlers() {
69
+ this.on = {
70
+ ...this.defaultEvents,
71
+ ...this.events,
72
+ };
22
73
  }
23
74
 
24
75
  constructor(params) {
25
- this.delegateTypes = [];
26
- this.userActions = [];
76
+ this.defaultEvents = {
77
+ [constantsToBeMigrated.defaultEvents.ON_CREATE]: {
78
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
79
+ handler: this.onCreate,
80
+ },
81
+ [constantsToBeMigrated.defaultEvents.ON_UPDATE]: {
82
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
83
+ handler: this.onUpdate,
84
+ },
85
+ [constantsToBeMigrated.defaultEvents.ON_DELETE]: {
86
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
87
+ handler: this.onDelete,
88
+ },
89
+ [constantsToBeMigrated.defaultEvents.GET_CONFIG_OPTIONS]: {
90
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
91
+ handler: this.getConfigOptions,
92
+ },
93
+ [constantsToBeMigrated.defaultEvents.REFRESH_CONFIG_OPTIONS]: {
94
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
95
+ handler: this.refreshConfigOptions,
96
+ },
97
+ [constantsToBeMigrated.defaultEvents.GET_USER_ACTIONS]: {
98
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
99
+ handler: this.loadUserActions,
100
+ },
101
+ [constantsToBeMigrated.defaultEvents.GET_USER_ACTION_OPTIONS]: {
102
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
103
+ handler: this.getActionOptions,
104
+ },
105
+ [constantsToBeMigrated.defaultEvents.REFRESH_USER_ACTION_OPTIONS]: {
106
+ type: constantsToBeMigrated.types.LIFE_CYCLE_EVENT,
107
+ handler: this.refreshActionOptions,
108
+ },
109
+ };
110
+ this.loadModules();
27
111
  }
28
112
 
29
- //psuedo delegate for backwards compatability
30
- async receiveNotification(notifier, delegateString, object = null) {}
31
-
32
- async notify(delegateString, object = null) {
33
- if (!this.delegateTypes.includes(delegateString)) {
113
+ async send(event, object) {
114
+ if (!this.on[event]) {
34
115
  throw new Error(
35
- `delegateString:${delegateString} is not defined in delegateTypes`
116
+ `Event ${event} is not defined in the Integration event object`
36
117
  );
37
118
  }
38
- return this.receiveNotification(this, delegateString, object);
119
+ return this.on[event].handler.call(this, object);
39
120
  }
40
121
 
41
122
  async validateConfig() {
@@ -69,28 +150,20 @@ class IntegrationBase {
69
150
  async testAuth() {
70
151
  let didAuthPass = true;
71
152
 
72
- try {
73
- await this.primary.testAuth();
74
- } catch {
75
- didAuthPass = false;
76
- this.record.messages.errors.push({
77
- title: 'Authentication Error',
78
- message: `There was an error with your ${this.primary.constructor.getName()} Entity.
153
+ for (const module of Object.keys(IntegrationBase.Definition.modules)) {
154
+ try {
155
+ await this[module].testAuth();
156
+ } catch {
157
+ didAuthPass = false;
158
+ this.record.messages.errors.push({
159
+ title: 'Authentication Error',
160
+ message: `There was an error with your ${this[
161
+ module
162
+ ].constructor.getName()} Entity.
79
163
  Please reconnect/re-authenticate, or reach out to Support for assistance.`,
80
- timestamp: Date.now(),
81
- });
82
- }
83
-
84
- try {
85
- await this.target.testAuth();
86
- } catch {
87
- didAuthPass = false;
88
- this.record.messages.errors.push({
89
- title: 'Authentication Error',
90
- message: `There was an error with your ${this.target.constructor.getName()} Entity.
91
- Please reconnect/re-authenticate, or reach out to Support for assistance.`,
92
- timestamp: Date.now(),
93
- });
164
+ timestamp: Date.now(),
165
+ });
166
+ }
94
167
  }
95
168
 
96
169
  if (!didAuthPass) {
@@ -115,15 +188,6 @@ class IntegrationBase {
115
188
  );
116
189
  }
117
190
 
118
- async getAndSetUserActions() {
119
- this.userActions = await this.getUserActions();
120
- if (this.record?.config) {
121
- this.record.config.userActions = this.userActions;
122
- await this.record.save();
123
- }
124
- return this.userActions;
125
- }
126
-
127
191
  /**
128
192
  * CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
129
193
  */
@@ -153,11 +217,34 @@ class IntegrationBase {
153
217
  return options;
154
218
  }
155
219
 
156
- async getUserActions() {
157
- return [];
220
+ async loadDynamicUserActions() {
221
+ // Child class should override this method to load dynamic user actions.
222
+ // Dynamic user actions should return in the same form a valid event object
223
+
224
+ return {};
225
+ }
226
+ async loadUserActions({ actionType } = {}) {
227
+ console.log('loadUserActions called with actionType:', actionType);
228
+ const userActions = {};
229
+ for (const [key, event] of Object.entries(this.events)) {
230
+ if (event.type === constantsToBeMigrated.types.USER_ACTION) {
231
+ if (!actionType || event.userActionType === actionType) {
232
+ userActions[key] = event;
233
+ }
234
+ }
235
+ }
236
+ const dynamicUserActions = await this.loadDynamicUserActions();
237
+ const filteredDynamicActions = actionType
238
+ ? Object.fromEntries(
239
+ Object.entries(dynamicUserActions).filter(
240
+ ([_, event]) => event.userActionType === actionType
241
+ )
242
+ )
243
+ : dynamicUserActions;
244
+ return { ...userActions, ...filteredDynamicActions };
158
245
  }
159
246
 
160
- async getActionOptions() {
247
+ async getActionOptions(actionId, data) {
161
248
  const options = {
162
249
  jsonSchema: {},
163
250
  uiSchema: {},
@@ -1,22 +1,28 @@
1
1
  const { ModuleFactory, Credential, Entity } = require('../module-plugin');
2
- const {IntegrationModel} = require("./integration-model");
2
+ const { IntegrationModel } = require('./integration-model');
3
3
  const _ = require('lodash');
4
4
 
5
-
6
5
  class IntegrationFactory {
7
6
  constructor(integrationClasses = []) {
8
7
  this.integrationClasses = integrationClasses;
9
8
  this.moduleFactory = new ModuleFactory(...this.getModules());
10
- this.integrationTypes = this.integrationClasses.map(IntegrationClass => IntegrationClass.getName());
11
- this.getIntegrationConfigs = this.integrationClasses.map(IntegrationClass => IntegrationClass.Config);
9
+ this.integrationTypes = this.integrationClasses.map(
10
+ (IntegrationClass) => IntegrationClass.getName()
11
+ );
12
+ this.getIntegrationDefinitions = this.integrationClasses.map(
13
+ (IntegrationClass) => IntegrationClass.Definition
14
+ );
12
15
  }
13
16
 
14
17
  async getIntegrationOptions() {
15
- const options = this.integrationClasses.map(IntegrationClass => IntegrationClass.Options);
18
+ const options = this.integrationClasses.map(
19
+ (IntegrationClass) => IntegrationClass
20
+ );
16
21
  return {
17
22
  entities: {
18
- primary: this.getPrimaryName(),
19
- options: options.map(val => val.get()),
23
+ options: options.map((IntegrationClass) =>
24
+ IntegrationClass.getOptionDetails()
25
+ ),
20
26
  authorized: [],
21
27
  },
22
28
  integrations: [],
@@ -24,65 +30,148 @@ class IntegrationFactory {
24
30
  }
25
31
 
26
32
  getModules() {
27
- return [... new Set(this.integrationClasses.map(integration =>
28
- Object.values(integration.modules)
29
- ).flat())];
33
+ return [
34
+ ...new Set(
35
+ this.integrationClasses
36
+ .map((integration) =>
37
+ Object.values(integration.Definition.modules).map(
38
+ (module) => module.definition
39
+ )
40
+ )
41
+ .flat()
42
+ ),
43
+ ];
30
44
  }
31
45
 
32
- getPrimaryName() {
33
- function findMostFrequentElement(array) {
34
- const frequencyMap = _.countBy(array);
35
- return _.maxBy(_.keys(frequencyMap), (element) => frequencyMap[element]);
36
- }
37
- const allModulesNames = _.flatten(this.integrationClasses.map(integration =>
38
- Object.values(integration.modules).map(module => module.getName())
39
- ));
40
- return findMostFrequentElement(allModulesNames);
41
- }
42
-
43
- getIntegrationClassDefByType(type) {
46
+ getIntegrationClassByType(type) {
44
47
  const integrationClassIndex = this.integrationTypes.indexOf(type);
45
48
  return this.integrationClasses[integrationClassIndex];
46
49
  }
50
+ getModuleTypesAndKeys(integrationClass) {
51
+ const moduleTypesAndKeys = {};
52
+ const moduleTypeCount = {};
53
+
54
+ if (integrationClass && integrationClass.Definition.modules) {
55
+ for (const [key, moduleClass] of Object.entries(
56
+ integrationClass.Definition.modules
57
+ )) {
58
+ if (
59
+ moduleClass &&
60
+ typeof moduleClass.definition.getName === 'function'
61
+ ) {
62
+ const moduleType = moduleClass.definition.getName();
63
+
64
+ // Check if this module type has already been seen
65
+ if (moduleType in moduleTypesAndKeys) {
66
+ throw new Error(
67
+ `Duplicate module type "${moduleType}" found in integration class definition.`
68
+ );
69
+ }
70
+
71
+ // Well how baout now
72
+
73
+ moduleTypesAndKeys[moduleType] = key;
74
+ moduleTypeCount[moduleType] =
75
+ (moduleTypeCount[moduleType] || 0) + 1;
76
+ }
77
+ }
78
+ }
79
+
80
+ // Check for any module types with count > 1
81
+ for (const [moduleType, count] of Object.entries(moduleTypeCount)) {
82
+ if (count > 1) {
83
+ throw new Error(
84
+ `Multiple instances of module type "${moduleType}" found in integration class definition.`
85
+ );
86
+ }
87
+ }
88
+
89
+ return moduleTypesAndKeys;
90
+ }
47
91
 
48
92
  async getInstanceFromIntegrationId(params) {
49
- const integrationRecord = await IntegrationHelper.getIntegrationById(params.integrationId);
50
- let {userId} = params;
93
+ const integrationRecord = await IntegrationHelper.getIntegrationById(
94
+ params.integrationId
95
+ );
96
+ let { userId } = params;
51
97
  if (!integrationRecord) {
52
- throw new Error(`No integration found by the ID of ${params.integrationId}`);
98
+ throw new Error(
99
+ `No integration found by the ID of ${params.integrationId}`
100
+ );
53
101
  }
54
102
 
55
103
  if (!userId) {
56
104
  userId = integrationRecord.user._id.toString();
57
- } else if (userId !== integrationRecord.user._id.toString()) {
58
- throw new Error(`Integration ${params.integrationId} does not belong to User ${userId}, ${integrationRecord.user.id.toString()}`);
105
+ } else if (userId.toString() !== integrationRecord.user.toString()) {
106
+ throw new Error(
107
+ `Integration ${
108
+ params.integrationId
109
+ } does not belong to User ${userId}, ${integrationRecord.user.toString()}`
110
+ );
59
111
  }
60
112
 
61
- const integrationClassDef = this.getIntegrationClassDefByType(integrationRecord.config.type);
62
- const instance = new integrationClassDef({
113
+ const integrationClass = this.getIntegrationClassByType(
114
+ integrationRecord.config.type
115
+ );
116
+
117
+ const instance = new integrationClass({
63
118
  userId,
64
119
  integrationId: params.integrationId,
65
120
  });
121
+
122
+ if (
123
+ integrationRecord.entityReference &&
124
+ Object.keys(integrationRecord.entityReference) > 0
125
+ ) {
126
+ // Use the specified entityReference to find the modules and load them according to their key
127
+ // entityReference will be a map of entityIds with their corresponding desired key
128
+ for (const [entityId, key] of Object.entries(
129
+ integrationRecord.entityReference
130
+ )) {
131
+ const moduleInstance =
132
+ await this.moduleFactory.getModuleInstanceFromEntityId(
133
+ entityId,
134
+ integrationRecord.user
135
+ );
136
+ instance[key] = moduleInstance;
137
+ }
138
+ } else {
139
+ // for each entity, get the moduleinstance and load them according to their keys
140
+ // If it's the first entity, load the moduleinstance into primary as well
141
+ // If it's the second entity, load the moduleinstance into target as well
142
+ const moduleTypesAndKeys =
143
+ this.getModuleTypesAndKeys(integrationClass);
144
+ for (let i = 0; i < integrationRecord.entities.length; i++) {
145
+ const entityId = integrationRecord.entities[i];
146
+ const moduleInstance =
147
+ await this.moduleFactory.getModuleInstanceFromEntityId(
148
+ entityId,
149
+ integrationRecord.user
150
+ );
151
+ const moduleType = moduleInstance.getName();
152
+ const key = moduleTypesAndKeys[moduleType];
153
+ instance[key] = moduleInstance;
154
+ if (i === 0) {
155
+ instance.primary = moduleInstance;
156
+ } else if (i === 1) {
157
+ instance.target = moduleInstance;
158
+ }
159
+ }
160
+ }
66
161
  instance.record = integrationRecord;
67
- instance.delegateTypes.push(...integrationClassDef.Config.events);
68
- instance.primary = await this.moduleFactory.getModuleInstanceFromEntityId(
69
- instance.record.entities[0],
70
- instance.record.user
71
- );
72
- instance.target = await this.moduleFactory.getModuleInstanceFromEntityId(
73
- instance.record.entities[1],
74
- instance.record.user
75
- );
76
162
 
77
163
  try {
78
- await instance.getAndSetUserActions();
79
- instance.delegateTypes.push(...Object.keys(instance.userActions));
80
- } catch(e) {
81
- instance.userActions = {};
164
+ const additionalUserActions =
165
+ await instance.loadDynamicUserActions();
166
+ instance.events = { ...instance.events, ...additionalUserActions };
167
+ } catch (e) {
82
168
  instance.record.status = 'ERROR';
83
169
  instance.record.messages.errors.push(e);
84
170
  await instance.record.save();
85
171
  }
172
+ // Register all of the event handlers
173
+
174
+ await instance.registerEventHandlers();
86
175
  return instance;
87
176
  }
88
177
 
@@ -93,12 +182,15 @@ class IntegrationFactory {
93
182
  config,
94
183
  version: '0.0.0',
95
184
  });
96
- return await this.getInstanceFromIntegrationId({integrationId: integrationRecord.id, userId});
185
+ return await this.getInstanceFromIntegrationId({
186
+ integrationId: integrationRecord.id,
187
+ userId,
188
+ });
97
189
  }
98
190
  }
99
191
 
100
192
  const IntegrationHelper = {
101
- getFormattedIntegration: async function(integrationRecord) {
193
+ getFormattedIntegration: async function (integrationRecord) {
102
194
  const integrationObj = {
103
195
  id: integrationRecord.id,
104
196
  status: integrationRecord.status,
@@ -122,14 +214,19 @@ const IntegrationHelper = {
122
214
  return integrationObj;
123
215
  },
124
216
 
125
- getIntegrationsForUserId: async function(userId) {
217
+ getIntegrationsForUserId: async function (userId) {
126
218
  const integrationList = await IntegrationModel.find({ user: userId });
127
- return await Promise.all(integrationList.map(async (integrationRecord) =>
128
- await IntegrationHelper.getFormattedIntegration(integrationRecord)
129
- ));
219
+ return await Promise.all(
220
+ integrationList.map(
221
+ async (integrationRecord) =>
222
+ await IntegrationHelper.getFormattedIntegration(
223
+ integrationRecord
224
+ )
225
+ )
226
+ );
130
227
  },
131
228
 
132
- deleteIntegrationForUserById: async function(userId, integrationId) {
229
+ deleteIntegrationForUserById: async function (userId, integrationId) {
133
230
  const integrationList = await IntegrationModel.find({
134
231
  user: userId,
135
232
  _id: integrationId,
@@ -142,13 +239,13 @@ const IntegrationHelper = {
142
239
  await IntegrationModel.deleteOne({ _id: integrationId });
143
240
  },
144
241
 
145
- getIntegrationById: async function(id) {
146
- return IntegrationModel.findById(id);
242
+ getIntegrationById: async function (id) {
243
+ return IntegrationModel.findById(id).populate('entities');
147
244
  },
148
245
 
149
- listCredentials: async function(options) {
246
+ listCredentials: async function (options) {
150
247
  return Credential.find(options);
151
- }
152
- }
248
+ },
249
+ };
153
250
 
154
251
  module.exports = { IntegrationFactory, IntegrationHelper };
@@ -9,6 +9,10 @@ const schema = new mongoose.Schema(
9
9
  required: true,
10
10
  },
11
11
  ],
12
+ entityReference: {
13
+ type: mongoose.Schema.Types.Map,
14
+ of: String,
15
+ },
12
16
  user: {
13
17
  type: mongoose.Schema.Types.ObjectId,
14
18
  ref: 'User',