@friggframework/core 2.0.0--canary.397.fadaaf6.0 → 2.0.0--canary.397.b7e1978.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.
- package/credential/credential-repository.js +33 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/integrations/integration-base.js +1 -16
- package/integrations/integration-router.js +2 -0
- package/integrations/integration.js +19 -6
- package/modules/module-repository.js +11 -1
- package/modules/module-service.js +2 -3
- package/modules/module.js +40 -110
- package/modules/utils/map-module-dto.js +1 -1
- package/package.json +5 -5
|
@@ -4,6 +4,39 @@ class CredentialRepository {
|
|
|
4
4
|
async findCredentialById(id) {
|
|
5
5
|
return Credential.findById(id);
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
async updateAuthenticationStatus(credentialId, authIsValid) {
|
|
9
|
+
return Credential.updateOne({ _id: credentialId }, { $set: { auth_is_valid: authIsValid } });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Permanently remove a credential document.
|
|
14
|
+
* @param {string} credentialId
|
|
15
|
+
* @returns {Promise<import('mongoose').DeleteResult>}
|
|
16
|
+
*/
|
|
17
|
+
async deleteCredentialById(credentialId) {
|
|
18
|
+
return Credential.deleteOne({ _id: credentialId });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a new credential or update an existing one matching the identifiers.
|
|
23
|
+
* `credentialDetails` format: { identifiers: { ... }, details: { ... } }
|
|
24
|
+
* Identifiers are used in the query filter; details are merged into the document.
|
|
25
|
+
* @param {{identifiers: Object, details: Object}} credentialDetails
|
|
26
|
+
* @returns {Promise<Object>} The persisted credential (lean object)
|
|
27
|
+
*/
|
|
28
|
+
async upsertCredential(credentialDetails) {
|
|
29
|
+
const { identifiers, details } = credentialDetails;
|
|
30
|
+
if (!identifiers) throw new Error('identifiers required to upsert credential');
|
|
31
|
+
|
|
32
|
+
const query = { ...identifiers };
|
|
33
|
+
|
|
34
|
+
const update = { $set: { ...details } };
|
|
35
|
+
|
|
36
|
+
const options = { upsert: true, new: true, setDefaultsOnInsert: true, lean: true };
|
|
37
|
+
|
|
38
|
+
return Credential.findOneAndUpdate(query, update, options);
|
|
39
|
+
}
|
|
7
40
|
}
|
|
8
41
|
|
|
9
42
|
module.exports = { CredentialRepository };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class UpdateAuthenticationStatus {
|
|
2
|
+
constructor({ credentialRepository }) {
|
|
3
|
+
this.credentialRepository = credentialRepository;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} credentialId
|
|
8
|
+
* @param {boolean} authIsValid
|
|
9
|
+
*/
|
|
10
|
+
async execute(credentialId, authIsValid) {
|
|
11
|
+
await this.credentialRepository.updateAuthenticationStatus(credentialId, authIsValid);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { UpdateAuthenticationStatus };
|
|
@@ -61,21 +61,7 @@ class IntegrationBase {
|
|
|
61
61
|
static getCurrentVersion() {
|
|
62
62
|
return this.Definition.version;
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
// Load all the modules defined in Definition.modules
|
|
66
|
-
const moduleNames = Object.keys(this.constructor.Definition.modules);
|
|
67
|
-
for (const moduleName of moduleNames) {
|
|
68
|
-
const { definition } =
|
|
69
|
-
this.constructor.Definition.modules[moduleName];
|
|
70
|
-
if (typeof definition.API === 'function') {
|
|
71
|
-
this[moduleName] = { api: new definition.API({}) };
|
|
72
|
-
} else {
|
|
73
|
-
throw new Error(
|
|
74
|
-
`Module ${moduleName} must be a function that extends IntegrationModule`
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
64
|
+
|
|
79
65
|
registerEventHandlers() {
|
|
80
66
|
this.on = {
|
|
81
67
|
...this.defaultEvents,
|
|
@@ -119,7 +105,6 @@ class IntegrationBase {
|
|
|
119
105
|
handler: this.refreshActionOptions,
|
|
120
106
|
},
|
|
121
107
|
};
|
|
122
|
-
this.loadModules();
|
|
123
108
|
}
|
|
124
109
|
|
|
125
110
|
async send(event, object) {
|
|
@@ -51,9 +51,11 @@ function createIntegrationRouter(params) {
|
|
|
51
51
|
moduleService,
|
|
52
52
|
moduleRepository,
|
|
53
53
|
});
|
|
54
|
+
|
|
54
55
|
const getCredentialForUser = new GetCredentialForUser({
|
|
55
56
|
credentialRepository,
|
|
56
57
|
});
|
|
58
|
+
|
|
57
59
|
const createIntegration = new CreateIntegration({
|
|
58
60
|
integrationRepository,
|
|
59
61
|
integrationClasses,
|
|
@@ -62,8 +62,14 @@ class Integration {
|
|
|
62
62
|
// Integration behavior (strategy pattern)
|
|
63
63
|
this.integrationClass = integrationClass;
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
|
|
65
|
+
// Preserve the provided Module instances (array). We'll attach them
|
|
66
|
+
// as keyed modules after the behaviour instance is created so that
|
|
67
|
+
// `this.hubspot`, `this.salesforce`, etc. reference the fully-
|
|
68
|
+
// initialised objects with credentials.
|
|
69
|
+
this._moduleInstances = Array.isArray(modules) ? modules : [];
|
|
70
|
+
|
|
71
|
+
// Normalised map of modules keyed by module name (filled later)
|
|
72
|
+
this.modules = {};
|
|
67
73
|
|
|
68
74
|
// Initialize basic behavior (sync parts only)
|
|
69
75
|
this._initializeBasicBehavior();
|
|
@@ -110,10 +116,17 @@ class Integration {
|
|
|
110
116
|
this.events = this.behavior.events || {};
|
|
111
117
|
this.defaultEvents = this.behavior.defaultEvents || {};
|
|
112
118
|
|
|
119
|
+
// -----------------------------------------------------------------
|
|
120
|
+
// Inject the real Module instances (with credentials) so that any
|
|
121
|
+
// behaviour code accessing `this.<moduleName>.api` hits the
|
|
122
|
+
// correctly authenticated requester created by ModuleService.
|
|
123
|
+
// -----------------------------------------------------------------
|
|
124
|
+
for (const mod of this._moduleInstances) {
|
|
125
|
+
const key = typeof mod.getName === 'function' ? mod.getName() : mod.name;
|
|
126
|
+
if (!key) continue;
|
|
127
|
+
this.setModule(key, mod);
|
|
128
|
+
}
|
|
113
129
|
|
|
114
|
-
// Expose behaviour instance methods directly on the wrapper so that
|
|
115
|
-
// early-bound handlers (created before behaviour existed) can still
|
|
116
|
-
// access them without falling back through the Proxy. This prevents
|
|
117
130
|
// `undefined` errors for methods like `loadDynamicUserActions` that
|
|
118
131
|
// may be invoked inside default event-handlers.
|
|
119
132
|
let proto = Object.getPrototypeOf(this.behavior);
|
|
@@ -217,4 +230,4 @@ class Integration {
|
|
|
217
230
|
}
|
|
218
231
|
}
|
|
219
232
|
|
|
220
|
-
module.exports = { Integration };
|
|
233
|
+
module.exports = { Integration };
|
|
@@ -42,7 +42,7 @@ class ModuleRepository {
|
|
|
42
42
|
async findEntitiesByUserId(userId) {
|
|
43
43
|
const entitiesRecords = await Entity.find(
|
|
44
44
|
{ user: userId },
|
|
45
|
-
'
|
|
45
|
+
'',
|
|
46
46
|
{ lean: true }
|
|
47
47
|
).populate('credential');
|
|
48
48
|
|
|
@@ -76,6 +76,16 @@ class ModuleRepository {
|
|
|
76
76
|
moduleName: e.moduleName,
|
|
77
77
|
}));
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Remove the credential reference from an Entity document without loading a full Mongoose instance.
|
|
82
|
+
* Useful when a credential has been revoked/deleted (e.g. via Module.deauthorize).
|
|
83
|
+
* @param {string} entityId
|
|
84
|
+
* @returns {Promise<import('mongoose').UpdateWriteOpResult>}
|
|
85
|
+
*/
|
|
86
|
+
async unsetCredential(entityId) {
|
|
87
|
+
return Entity.updateOne({ _id: entityId }, { $unset: { credential: "" } });
|
|
88
|
+
}
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
module.exports = { ModuleRepository };
|
|
@@ -27,10 +27,9 @@ class ModuleService {
|
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const moduleName = entity.moduleName;
|
|
31
31
|
const moduleDefinition = this.moduleDefinitions.find((def) => {
|
|
32
|
-
|
|
33
|
-
return entityType === modelName;
|
|
32
|
+
return moduleName === def.modelName;
|
|
34
33
|
});
|
|
35
34
|
|
|
36
35
|
if (!moduleDefinition) {
|
package/modules/module.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
const { Delegate } = require('../core');
|
|
2
|
-
const { get } = require('../assertions');
|
|
3
2
|
const _ = require('lodash');
|
|
4
3
|
const { flushDebugLog } = require('../logs');
|
|
5
|
-
const { Credential } = require('./credential');
|
|
6
|
-
const { Entity } = require('./entity');
|
|
7
|
-
const { mongoose } = require('../database/mongoose');
|
|
8
4
|
const { ModuleConstants } = require('./ModuleConstants');
|
|
9
5
|
|
|
6
|
+
// todo: this class should be a Domain class, and the Delegate function is preventing us from
|
|
7
|
+
// doing that, we probably have to get rid of the Delegate class as well as the event based
|
|
8
|
+
// calls since they go against the Domain Driven Design principles (eg. a domain class should not call repository methods or use cases)
|
|
10
9
|
class Module extends Delegate {
|
|
11
10
|
|
|
12
11
|
//todo: entity should be replaced with actual entity properties
|
|
@@ -30,12 +29,15 @@ class Module extends Delegate {
|
|
|
30
29
|
this.modelName = this.definition.modelName;
|
|
31
30
|
this.apiClass = this.definition.API;
|
|
32
31
|
|
|
32
|
+
// Repository used for persistence operations related to credentials.
|
|
33
|
+
const { CredentialRepository } = require('../credential/credential-repository');
|
|
34
|
+
this.credentialRepository = new CredentialRepository();
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
-
this.EntityModel = this.getEntityModel();
|
|
36
|
+
// Repository responsible for entity persistence actions
|
|
37
|
+
const { ModuleRepository } = require('./module-repository');
|
|
38
|
+
this.moduleRepository = new ModuleRepository();
|
|
38
39
|
|
|
40
|
+
Object.assign(this, this.definition.requiredAuthMethods);
|
|
39
41
|
|
|
40
42
|
const apiParams = {
|
|
41
43
|
...this.definition.env,
|
|
@@ -46,11 +48,6 @@ class Module extends Delegate {
|
|
|
46
48
|
this.api = new this.apiClass(apiParams);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
static getEntityModelFromDefinition(definition) {
|
|
50
|
-
const partialModule = new this({ definition });
|
|
51
|
-
return partialModule.getEntityModel();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
51
|
getName() {
|
|
55
52
|
return this.name;
|
|
56
53
|
}
|
|
@@ -72,45 +69,6 @@ class Module extends Delegate {
|
|
|
72
69
|
return _.pick(entity, ...this.apiPropertiesToPersist?.entity);
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
getEntityModel() {
|
|
76
|
-
if (!this.EntityModel) {
|
|
77
|
-
const prefix = this.modelName ?? _.upperFirst(this.getName());
|
|
78
|
-
const arrayToDefaultObject = (array, defaultValue) =>
|
|
79
|
-
_.mapValues(_.keyBy(array), () => defaultValue);
|
|
80
|
-
const schema = new mongoose.Schema(
|
|
81
|
-
arrayToDefaultObject(this.apiPropertiesToPersist.entity, {
|
|
82
|
-
type: mongoose.Schema.Types.Mixed,
|
|
83
|
-
trim: true,
|
|
84
|
-
})
|
|
85
|
-
);
|
|
86
|
-
const name = `${prefix}Entity`;
|
|
87
|
-
this.EntityModel =
|
|
88
|
-
Entity.discriminators?.[name] ||
|
|
89
|
-
Entity.discriminator(name, schema);
|
|
90
|
-
}
|
|
91
|
-
return this.EntityModel;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
getCredentialModel() {
|
|
95
|
-
if (!this.CredentialModel) {
|
|
96
|
-
const arrayToDefaultObject = (array, defaultValue) =>
|
|
97
|
-
_.mapValues(_.keyBy(array), () => defaultValue);
|
|
98
|
-
const schema = new mongoose.Schema(
|
|
99
|
-
arrayToDefaultObject(this.apiPropertiesToPersist.credential, {
|
|
100
|
-
type: mongoose.Schema.Types.Mixed,
|
|
101
|
-
trim: true,
|
|
102
|
-
lhEncrypt: true,
|
|
103
|
-
})
|
|
104
|
-
);
|
|
105
|
-
const prefix = this.modelName ?? _.upperFirst(this.getName());
|
|
106
|
-
const name = `${prefix}Credential`;
|
|
107
|
-
this.CredentialModel =
|
|
108
|
-
Credential.discriminators?.[name] ||
|
|
109
|
-
Credential.discriminator(name, schema);
|
|
110
|
-
}
|
|
111
|
-
return this.CredentialModel;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
72
|
async validateAuthorizationRequirements() {
|
|
115
73
|
const requirements = await this.getAuthorizationRequirements();
|
|
116
74
|
let valid = true;
|
|
@@ -177,7 +135,9 @@ class Module extends Delegate {
|
|
|
177
135
|
this.apiParamsFromCredential(this.api)
|
|
178
136
|
);
|
|
179
137
|
credentialDetails.details.auth_is_valid = true;
|
|
180
|
-
|
|
138
|
+
|
|
139
|
+
const persisted = await this.credentialRepository.upsertCredential(credentialDetails);
|
|
140
|
+
this.credential = persisted;
|
|
181
141
|
}
|
|
182
142
|
|
|
183
143
|
async receiveNotification(notifier, delegateString, object = null) {
|
|
@@ -190,73 +150,43 @@ class Module extends Delegate {
|
|
|
190
150
|
}
|
|
191
151
|
}
|
|
192
152
|
|
|
193
|
-
async
|
|
194
|
-
|
|
195
|
-
const details = get(entityDetails, 'details');
|
|
196
|
-
const search = await this.EntityModel.find(identifiers);
|
|
197
|
-
if (search.length > 1) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
'Multiple entities found with the same identifiers: ' +
|
|
200
|
-
JSON.stringify(identifiers)
|
|
201
|
-
);
|
|
202
|
-
} else if (search.length === 0) {
|
|
203
|
-
this.entity = await this.EntityModel.create({
|
|
204
|
-
credential: this.credential.id,
|
|
205
|
-
...details,
|
|
206
|
-
...identifiers,
|
|
207
|
-
});
|
|
208
|
-
} else if (search.length === 1) {
|
|
209
|
-
this.entity = search[0];
|
|
210
|
-
}
|
|
211
|
-
if (this.entity.credential === undefined) {
|
|
212
|
-
this.entity.credential = this.credential.id;
|
|
213
|
-
await this.entity.save();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
153
|
+
async markCredentialsInvalid() {
|
|
154
|
+
if (!this.credential) return;
|
|
216
155
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
156
|
+
// Persist flag change through repository – works even when the
|
|
157
|
+
// credential object is a plain JavaScript object (lean query).
|
|
158
|
+
const credentialId = this.credential._id || this.credential.id;
|
|
159
|
+
if (!credentialId) return;
|
|
220
160
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
);
|
|
225
|
-
if (credentialSearch.length > 1) {
|
|
226
|
-
throw new Error(
|
|
227
|
-
`Multiple credentials found with same identifiers: ${identifiers}`
|
|
228
|
-
);
|
|
229
|
-
} else if (credentialSearch.length === 1) {
|
|
230
|
-
// found exactly one credential with these identifiers
|
|
231
|
-
this.credential = credentialSearch[0];
|
|
232
|
-
} else {
|
|
233
|
-
// found no credential with these identifiers (match none for insert)
|
|
234
|
-
this.credential = { $exists: false };
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// update credential or create if none was found
|
|
238
|
-
this.credential = await this.CredentialModel.findOneAndUpdate(
|
|
239
|
-
{ _id: this.credential },
|
|
240
|
-
{ $set: { ...identifiers, ...details } },
|
|
241
|
-
{ useFindAndModify: true, new: true, upsert: true }
|
|
161
|
+
await this.credentialRepository.updateAuthenticationStatus(
|
|
162
|
+
credentialId,
|
|
163
|
+
false
|
|
242
164
|
);
|
|
243
|
-
}
|
|
244
165
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
await this.credential.save();
|
|
249
|
-
}
|
|
166
|
+
// Keep the in-memory snapshot consistent so that callers can read the
|
|
167
|
+
// updated state without another fetch.
|
|
168
|
+
this.credential.auth_is_valid = false;
|
|
250
169
|
}
|
|
251
170
|
|
|
252
171
|
async deauthorize() {
|
|
253
172
|
this.api = new this.apiClass();
|
|
173
|
+
|
|
174
|
+
// Remove persisted credential (if any)
|
|
254
175
|
if (this.entity?.credential) {
|
|
255
|
-
|
|
256
|
-
_id
|
|
257
|
-
|
|
176
|
+
const credentialId =
|
|
177
|
+
this.entity.credential._id || this.entity.credential.id || this.entity.credential;
|
|
178
|
+
|
|
179
|
+
// Delete credential via repository
|
|
180
|
+
await this.credentialRepository.deleteCredentialById(credentialId);
|
|
181
|
+
|
|
182
|
+
// Unset credential reference on the Entity document
|
|
183
|
+
const entityId = this.entity._id || this.entity.id;
|
|
184
|
+
if (entityId) {
|
|
185
|
+
await this.moduleRepository.unsetCredential(entityId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Keep in-memory snapshot consistent
|
|
258
189
|
this.entity.credential = undefined;
|
|
259
|
-
await this.entity.save();
|
|
260
190
|
}
|
|
261
191
|
}
|
|
262
192
|
|
|
@@ -6,7 +6,7 @@ function mapModuleClassToModuleDTO(moduleInstance) {
|
|
|
6
6
|
if (!moduleInstance) return null;
|
|
7
7
|
|
|
8
8
|
return {
|
|
9
|
-
id: moduleInstance.entity.
|
|
9
|
+
id: moduleInstance.entity.id,
|
|
10
10
|
name: moduleInstance.name,
|
|
11
11
|
userId: moduleInstance.userId,
|
|
12
12
|
entity: moduleInstance.entity,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.397.
|
|
4
|
+
"version": "2.0.0--canary.397.b7e1978.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hapi/boom": "^10.0.1",
|
|
7
7
|
"aws-sdk": "^2.1200.0",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"uuid": "^9.0.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@friggframework/eslint-config": "2.0.0--canary.397.
|
|
26
|
-
"@friggframework/prettier-config": "2.0.0--canary.397.
|
|
27
|
-
"@friggframework/test": "2.0.0--canary.397.
|
|
25
|
+
"@friggframework/eslint-config": "2.0.0--canary.397.b7e1978.0",
|
|
26
|
+
"@friggframework/prettier-config": "2.0.0--canary.397.b7e1978.0",
|
|
27
|
+
"@friggframework/test": "2.0.0--canary.397.b7e1978.0",
|
|
28
28
|
"@types/lodash": "4.17.15",
|
|
29
29
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
30
30
|
"chai": "^4.3.6",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
},
|
|
54
54
|
"homepage": "https://github.com/friggframework/frigg#readme",
|
|
55
55
|
"description": "",
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "b7e197879d90d5bd782c5299aa55f312eb7d4b63"
|
|
57
57
|
}
|