@friggframework/core 2.0.0--canary.398.a314355.0 → 2.0.0--canary.397.4957a89.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/README.md +931 -50
- package/core/create-handler.js +1 -0
- package/credential/credential-repository.js +42 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/models/WebsocketConnection.js +0 -5
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/backend-utils.js +35 -34
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +25 -5
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +1 -16
- package/integrations/index.js +0 -5
- package/integrations/integration-base.js +42 -44
- package/integrations/integration-repository.js +67 -0
- package/integrations/integration-router.js +301 -178
- package/integrations/integration.js +233 -0
- package/integrations/options.js +1 -1
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +89 -0
- package/integrations/tests/use-cases/create-integration.test.js +124 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +143 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +143 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +169 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +169 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +134 -0
- package/integrations/use-cases/create-integration.js +72 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -0
- package/integrations/use-cases/get-integration-for-user.js +79 -0
- package/integrations/use-cases/get-integration-instance.js +84 -0
- package/integrations/use-cases/get-integrations-for-user.js +77 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/index.js +11 -0
- package/integrations/use-cases/update-integration-messages.js +31 -0
- package/integrations/use-cases/update-integration-status.js +28 -0
- package/integrations/use-cases/update-integration.js +92 -0
- package/integrations/utils/map-integration-dto.js +36 -0
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +54 -0
- package/modules/module-repository.js +107 -0
- package/modules/module.js +221 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +19 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +58 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +56 -0
- package/modules/use-cases/process-authorization-callback.js +114 -0
- package/modules/use-cases/refresh-entity-options.js +58 -0
- package/modules/use-cases/test-module-auth.js +54 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +5 -5
- package/syncs/sync.js +0 -1
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +4 -56
- package/types/syncs/index.d.ts +0 -2
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user-repository.js +62 -0
- package/user/user.js +77 -0
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/module-plugin/auther.js +0 -393
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/credential.js +0 -0
- /package/{module-plugin → modules}/entity.js +0 -0
- /package/{module-plugin → modules}/requester/api-key.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/requester/requester.js +0 -0
- /package/{module-plugin → modules}/requester/requester.test.js +0 -0
- /package/{module-plugin → modules}/test/auther.test.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const { Options } = require('./options');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration (Domain Aggregate-Root)
|
|
5
|
+
* ----------------------------------
|
|
6
|
+
* This class represents a *configured* integration instance at runtime. It is
|
|
7
|
+
* deliberately split into **two layers**:
|
|
8
|
+
* 1. A *data snapshot* of the persisted record (id, userId, config, etc.).
|
|
9
|
+
* 2. A *behaviour* object: a concrete class supplied by the app developer
|
|
10
|
+
* that extends `IntegrationBase` and implements event handlers, user
|
|
11
|
+
* actions, custom routes, etc.
|
|
12
|
+
*
|
|
13
|
+
* The two layers are glued together via a **JavaScript `Proxy`**. When a
|
|
14
|
+
* property is requested on an `Integration` instance we:
|
|
15
|
+
* • Check if the property exists on the wrapper itself (data-layer).
|
|
16
|
+
* • Fallback to the behaviour instance (logic-layer).
|
|
17
|
+
* • If the value is a function we `.bind(this)` so that the function's
|
|
18
|
+
* `this` always points to the *wrapper* – giving it access to both data
|
|
19
|
+
* and behaviour transparently.
|
|
20
|
+
*
|
|
21
|
+
* This means you can treat a hydrated Integration as if it *were* the custom
|
|
22
|
+
* class:
|
|
23
|
+
*
|
|
24
|
+
* ```js
|
|
25
|
+
* const integration = await getIntegration.execute(id, userId);
|
|
26
|
+
* // `send` actually lives on IntegrationBase but is accessible here
|
|
27
|
+
* const actions = await integration.send('GET_USER_ACTIONS');
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* A corollary benefit is that **circular references stay internal**: the heavy
|
|
31
|
+
* `Module → Api → delegate` graph is never exposed when we later serialise the
|
|
32
|
+
* object to JSON – we map it to a DTO first.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Integration Domain Entity
|
|
37
|
+
* Represents a configured integration with its data and behavior
|
|
38
|
+
* Uses the strategy pattern to delegate behavior to the integration class
|
|
39
|
+
* This is the main class that is used to interact with integrations
|
|
40
|
+
*/
|
|
41
|
+
class Integration {
|
|
42
|
+
constructor({
|
|
43
|
+
id,
|
|
44
|
+
userId,
|
|
45
|
+
entities,
|
|
46
|
+
config,
|
|
47
|
+
status,
|
|
48
|
+
version,
|
|
49
|
+
messages,
|
|
50
|
+
integrationClass,
|
|
51
|
+
modules = {}
|
|
52
|
+
}) {
|
|
53
|
+
// Data from record
|
|
54
|
+
this.id = id;
|
|
55
|
+
this.userId = userId;
|
|
56
|
+
this.entities = entities;
|
|
57
|
+
this.config = config;
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.version = version;
|
|
60
|
+
this.messages = messages;
|
|
61
|
+
|
|
62
|
+
// Integration behavior (strategy pattern)
|
|
63
|
+
this.integrationClass = integrationClass;
|
|
64
|
+
|
|
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 = {};
|
|
73
|
+
|
|
74
|
+
// Initialize basic behavior (sync parts only)
|
|
75
|
+
this._initializeBasicBehavior();
|
|
76
|
+
|
|
77
|
+
// --- Behaviour delegation via Proxy --------------------------------
|
|
78
|
+
// The Proxy merges the *data layer* (this wrapper) with the *behaviour
|
|
79
|
+
// layer* (custom IntegrationBase subclass). Consumers don't have to
|
|
80
|
+
// know (or care) where a method/property is defined.
|
|
81
|
+
return new Proxy(this, {
|
|
82
|
+
get(target, prop) {
|
|
83
|
+
// First, check if property exists on Integration entity
|
|
84
|
+
if (prop in target) {
|
|
85
|
+
return target[prop];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Then, check if it exists on the behavior instance
|
|
89
|
+
if (target.behavior && prop in target.behavior) {
|
|
90
|
+
const value = target.behavior[prop];
|
|
91
|
+
|
|
92
|
+
// If it's a function, bind the context to the Integration entity
|
|
93
|
+
if (typeof value === 'function') {
|
|
94
|
+
return value.bind(target);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Return undefined for non-existent properties
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_initializeBasicBehavior() {
|
|
107
|
+
// Initialize basic behavior (sync parts only)
|
|
108
|
+
if (this.integrationClass) {
|
|
109
|
+
// Create instance for behavior delegation
|
|
110
|
+
this.behavior = new this.integrationClass({
|
|
111
|
+
userId: this.userId,
|
|
112
|
+
integrationId: this.id
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Copy events
|
|
116
|
+
this.events = this.behavior.events || {};
|
|
117
|
+
this.defaultEvents = this.behavior.defaultEvents || {};
|
|
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 ModuleFactory.
|
|
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
|
+
}
|
|
129
|
+
|
|
130
|
+
// `undefined` errors for methods like `loadDynamicUserActions` that
|
|
131
|
+
// may be invoked inside default event-handlers.
|
|
132
|
+
let proto = Object.getPrototypeOf(this.behavior);
|
|
133
|
+
while (proto && proto !== Object.prototype) {
|
|
134
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
135
|
+
if (key === 'constructor') continue;
|
|
136
|
+
if (typeof proto[key] === 'function' && this[key] === undefined) {
|
|
137
|
+
this[key] = proto[key].bind(this.behavior);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
proto = Object.getPrototypeOf(proto);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async initialize() {
|
|
146
|
+
// Complete async initialization
|
|
147
|
+
if (this.behavior) {
|
|
148
|
+
// Load dynamic user actions
|
|
149
|
+
try {
|
|
150
|
+
const additionalUserActions = await this.loadDynamicUserActions();
|
|
151
|
+
this.events = { ...this.events, ...additionalUserActions };
|
|
152
|
+
} catch (e) {
|
|
153
|
+
this.addError(e);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Register event handlers
|
|
157
|
+
await this.registerEventHandlers();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Core methods that should always be on Integration entity
|
|
162
|
+
// These override any behavior methods with the same name
|
|
163
|
+
|
|
164
|
+
getConfig() {
|
|
165
|
+
return this.config;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Module access helpers
|
|
169
|
+
getModule(key) {
|
|
170
|
+
return this.modules[key];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setModule(key, module) {
|
|
174
|
+
this.modules[key] = module;
|
|
175
|
+
// Also set on behavior for backward compatibility
|
|
176
|
+
if (this.behavior) {
|
|
177
|
+
this.behavior[key] = module;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// State management
|
|
182
|
+
addError(error) {
|
|
183
|
+
if (!this.messages.errors) {
|
|
184
|
+
this.messages.errors = [];
|
|
185
|
+
}
|
|
186
|
+
this.messages.errors.push(error);
|
|
187
|
+
this.status = 'ERROR';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
addWarning(warning) {
|
|
191
|
+
if (!this.messages.warnings) {
|
|
192
|
+
this.messages.warnings = [];
|
|
193
|
+
}
|
|
194
|
+
this.messages.warnings.push(warning);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Domain methods
|
|
198
|
+
isActive() {
|
|
199
|
+
return this.status === 'ENABLED' || this.status === 'ACTIVE';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
needsConfiguration() {
|
|
203
|
+
return this.status === 'NEEDS_CONFIG';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
hasErrors() {
|
|
207
|
+
return this.status === 'ERROR';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
belongsToUser(userId) {
|
|
211
|
+
return this.userId.toString() === userId.toString();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get the underlying behavior instance (useful for debugging or special cases)
|
|
215
|
+
getBehavior() {
|
|
216
|
+
return this.behavior;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check if a method exists (either on entity or behavior)
|
|
220
|
+
hasMethod(methodName) {
|
|
221
|
+
return methodName in this || (this.behavior && methodName in this.behavior);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getOptionDetails() {
|
|
225
|
+
const options = new Options({
|
|
226
|
+
module: Object.values(this.integrationClass.Definition.modules)[0], // This is a placeholder until we revamp the frontend
|
|
227
|
+
...this.integrationClass.Definition,
|
|
228
|
+
});
|
|
229
|
+
return options.get();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = { Integration };
|
package/integrations/options.js
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const { IntegrationBase } = require('../../integration-base');
|
|
2
|
+
|
|
3
|
+
class DummyModule {
|
|
4
|
+
static definition = {
|
|
5
|
+
getName: () => 'dummy'
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class DummyIntegration extends IntegrationBase {
|
|
10
|
+
static Definition = {
|
|
11
|
+
name: 'dummy',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
modules: {
|
|
14
|
+
dummy: DummyModule
|
|
15
|
+
},
|
|
16
|
+
display: {
|
|
17
|
+
label: 'Dummy Integration',
|
|
18
|
+
description: 'A dummy integration for testing',
|
|
19
|
+
detailsUrl: 'https://example.com',
|
|
20
|
+
icon: 'dummy-icon'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static getOptionDetails() {
|
|
25
|
+
return {
|
|
26
|
+
name: this.Definition.name,
|
|
27
|
+
version: this.Definition.version,
|
|
28
|
+
display: this.Definition.display
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constructor(params) {
|
|
33
|
+
super(params);
|
|
34
|
+
this.sendSpy = jest.fn();
|
|
35
|
+
this.eventCallHistory = [];
|
|
36
|
+
this.events = {};
|
|
37
|
+
|
|
38
|
+
this.integrationRepository = {
|
|
39
|
+
updateIntegrationById: jest.fn().mockResolvedValue({}),
|
|
40
|
+
findIntegrationById: jest.fn().mockResolvedValue({}),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.updateIntegrationStatus = {
|
|
44
|
+
execute: jest.fn().mockResolvedValue({})
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this.updateIntegrationMessages = {
|
|
48
|
+
execute: jest.fn().mockResolvedValue({})
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.registerEventHandlers();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async loadDynamicUserActions() {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async registerEventHandlers() {
|
|
59
|
+
super.registerEventHandlers();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async send(event, data) {
|
|
64
|
+
this.sendSpy(event, data);
|
|
65
|
+
this.eventCallHistory.push({ event, data, timestamp: Date.now() });
|
|
66
|
+
return super.send(event, data);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async initialize() {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async onCreate({ integrationId }) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async onUpdate(params) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async onDelete(params) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getConfig() {
|
|
86
|
+
return this.config || {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { DummyIntegration };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const { v4: uuid } = require('uuid');
|
|
2
|
+
|
|
3
|
+
class TestIntegrationRepository {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.store = new Map();
|
|
6
|
+
this.operationHistory = [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async createIntegration(entities, userId, config) {
|
|
10
|
+
const id = uuid();
|
|
11
|
+
const record = {
|
|
12
|
+
id,
|
|
13
|
+
_id: id,
|
|
14
|
+
entitiesIds: entities,
|
|
15
|
+
userId: userId,
|
|
16
|
+
config,
|
|
17
|
+
version: '0.0.0',
|
|
18
|
+
status: 'NEW',
|
|
19
|
+
messages: {},
|
|
20
|
+
};
|
|
21
|
+
this.store.set(id, record);
|
|
22
|
+
this.operationHistory.push({ operation: 'create', id, userId, config });
|
|
23
|
+
return record;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async findIntegrationById(id) {
|
|
27
|
+
const rec = this.store.get(id);
|
|
28
|
+
this.operationHistory.push({ operation: 'findById', id, found: !!rec });
|
|
29
|
+
if (!rec) return null;
|
|
30
|
+
return rec;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async findIntegrationsByUserId(userId) {
|
|
34
|
+
const results = Array.from(this.store.values()).filter(r => r.userId === userId);
|
|
35
|
+
this.operationHistory.push({ operation: 'findByUserId', userId, count: results.length });
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async updateIntegrationMessages(id, type, title, body, timestamp) {
|
|
40
|
+
const rec = this.store.get(id);
|
|
41
|
+
if (!rec) {
|
|
42
|
+
this.operationHistory.push({ operation: 'updateMessages', id, success: false });
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (!rec.messages[type]) rec.messages[type] = [];
|
|
46
|
+
rec.messages[type].push({ title, message: body, timestamp });
|
|
47
|
+
this.operationHistory.push({ operation: 'updateMessages', id, type, success: true });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async updateIntegrationConfig(id, config) {
|
|
52
|
+
const rec = this.store.get(id);
|
|
53
|
+
if (!rec) {
|
|
54
|
+
this.operationHistory.push({ operation: 'updateConfig', id, success: false });
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
rec.config = config;
|
|
58
|
+
this.operationHistory.push({ operation: 'updateConfig', id, success: true });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async deleteIntegrationById(id) {
|
|
63
|
+
const existed = this.store.has(id);
|
|
64
|
+
const result = this.store.delete(id);
|
|
65
|
+
this.operationHistory.push({ operation: 'delete', id, existed, success: result });
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async updateIntegrationStatus(id, status) {
|
|
70
|
+
const rec = this.store.get(id);
|
|
71
|
+
if (rec) {
|
|
72
|
+
rec.status = status;
|
|
73
|
+
this.operationHistory.push({ operation: 'updateStatus', id, status, success: true });
|
|
74
|
+
} else {
|
|
75
|
+
this.operationHistory.push({ operation: 'updateStatus', id, status, success: false });
|
|
76
|
+
}
|
|
77
|
+
return !!rec;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getOperationHistory() {
|
|
81
|
+
return [...this.operationHistory];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
clearHistory() {
|
|
85
|
+
this.operationHistory = [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { TestIntegrationRepository };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const { CreateIntegration } = require('../../use-cases/create-integration');
|
|
2
|
+
const { TestIntegrationRepository } = require('../doubles/test-integration-repository');
|
|
3
|
+
const { TestModuleFactory } = require('../../../modules/tests/doubles/test-module-factory');
|
|
4
|
+
const { DummyIntegration } = require('../doubles/dummy-integration-class');
|
|
5
|
+
|
|
6
|
+
describe('CreateIntegration Use-Case', () => {
|
|
7
|
+
let integrationRepository;
|
|
8
|
+
let moduleFactory;
|
|
9
|
+
let useCase;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
integrationRepository = new TestIntegrationRepository();
|
|
13
|
+
moduleFactory = new TestModuleFactory();
|
|
14
|
+
useCase = new CreateIntegration({
|
|
15
|
+
integrationRepository,
|
|
16
|
+
integrationClasses: [DummyIntegration],
|
|
17
|
+
moduleFactory,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('happy path', () => {
|
|
22
|
+
it('creates an integration and returns DTO', async () => {
|
|
23
|
+
const entities = ['entity-1'];
|
|
24
|
+
const userId = 'user-1';
|
|
25
|
+
const config = { type: 'dummy', foo: 'bar' };
|
|
26
|
+
|
|
27
|
+
const dto = await useCase.execute(entities, userId, config);
|
|
28
|
+
|
|
29
|
+
expect(dto.id).toBeDefined();
|
|
30
|
+
expect(dto.config).toEqual(config);
|
|
31
|
+
expect(dto.userId).toBe(userId);
|
|
32
|
+
expect(dto.entities).toEqual(entities);
|
|
33
|
+
expect(dto.status).toBe('NEW');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('triggers ON_CREATE event with correct payload', async () => {
|
|
37
|
+
const entities = ['entity-1'];
|
|
38
|
+
const userId = 'user-1';
|
|
39
|
+
const config = { type: 'dummy', foo: 'bar' };
|
|
40
|
+
|
|
41
|
+
const dto = await useCase.execute(entities, userId, config);
|
|
42
|
+
|
|
43
|
+
const record = await integrationRepository.findIntegrationById(dto.id);
|
|
44
|
+
expect(record).toBeTruthy();
|
|
45
|
+
|
|
46
|
+
const history = integrationRepository.getOperationHistory();
|
|
47
|
+
const createOperation = history.find(op => op.operation === 'create');
|
|
48
|
+
expect(createOperation).toEqual({
|
|
49
|
+
operation: 'create',
|
|
50
|
+
id: dto.id,
|
|
51
|
+
userId,
|
|
52
|
+
config
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('loads modules for each entity', async () => {
|
|
57
|
+
const entities = ['entity-1', 'entity-2'];
|
|
58
|
+
const userId = 'user-1';
|
|
59
|
+
const config = { type: 'dummy' };
|
|
60
|
+
|
|
61
|
+
const dto = await useCase.execute(entities, userId, config);
|
|
62
|
+
|
|
63
|
+
expect(dto.entities).toEqual(entities);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('error cases', () => {
|
|
68
|
+
it('throws error when integration class is not found', async () => {
|
|
69
|
+
const entities = ['entity-1'];
|
|
70
|
+
const userId = 'user-1';
|
|
71
|
+
const config = { type: 'unknown-type' };
|
|
72
|
+
|
|
73
|
+
await expect(useCase.execute(entities, userId, config))
|
|
74
|
+
.rejects
|
|
75
|
+
.toThrow('No integration class found for type: unknown-type');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('throws error when no integration classes provided', async () => {
|
|
79
|
+
const useCaseWithoutClasses = new CreateIntegration({
|
|
80
|
+
integrationRepository,
|
|
81
|
+
integrationClasses: [],
|
|
82
|
+
moduleFactory,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const entities = ['entity-1'];
|
|
86
|
+
const userId = 'user-1';
|
|
87
|
+
const config = { type: 'dummy' };
|
|
88
|
+
|
|
89
|
+
await expect(useCaseWithoutClasses.execute(entities, userId, config))
|
|
90
|
+
.rejects
|
|
91
|
+
.toThrow('No integration class found for type: dummy');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('edge cases', () => {
|
|
96
|
+
it('handles empty entities array', async () => {
|
|
97
|
+
const entities = [];
|
|
98
|
+
const userId = 'user-1';
|
|
99
|
+
const config = { type: 'dummy' };
|
|
100
|
+
|
|
101
|
+
const dto = await useCase.execute(entities, userId, config);
|
|
102
|
+
|
|
103
|
+
expect(dto.entities).toEqual([]);
|
|
104
|
+
expect(dto.id).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('handles complex config objects', async () => {
|
|
108
|
+
const entities = ['entity-1'];
|
|
109
|
+
const userId = 'user-1';
|
|
110
|
+
const config = {
|
|
111
|
+
type: 'dummy',
|
|
112
|
+
nested: {
|
|
113
|
+
value: 123,
|
|
114
|
+
array: [1, 2, 3],
|
|
115
|
+
bool: true
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const dto = await useCase.execute(entities, userId, config);
|
|
120
|
+
|
|
121
|
+
expect(dto.config).toEqual(config);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const { DeleteIntegrationForUser } = require('../../use-cases/delete-integration-for-user');
|
|
2
|
+
const { TestIntegrationRepository } = require('../doubles/test-integration-repository');
|
|
3
|
+
const { DummyIntegration } = require('../doubles/dummy-integration-class');
|
|
4
|
+
|
|
5
|
+
describe('DeleteIntegrationForUser Use-Case', () => {
|
|
6
|
+
let integrationRepository;
|
|
7
|
+
let useCase;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
integrationRepository = new TestIntegrationRepository();
|
|
11
|
+
useCase = new DeleteIntegrationForUser({
|
|
12
|
+
integrationRepository,
|
|
13
|
+
integrationClasses: [DummyIntegration],
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('happy path', () => {
|
|
18
|
+
it('deletes integration successfully', async () => {
|
|
19
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
20
|
+
|
|
21
|
+
await useCase.execute(record.id, 'user-1');
|
|
22
|
+
|
|
23
|
+
const found = await integrationRepository.findIntegrationById(record.id);
|
|
24
|
+
expect(found).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('tracks delete operation', async () => {
|
|
28
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
29
|
+
integrationRepository.clearHistory();
|
|
30
|
+
|
|
31
|
+
await useCase.execute(record.id, 'user-1');
|
|
32
|
+
|
|
33
|
+
const history = integrationRepository.getOperationHistory();
|
|
34
|
+
const deleteOperation = history.find(op => op.operation === 'delete');
|
|
35
|
+
expect(deleteOperation).toEqual({
|
|
36
|
+
operation: 'delete',
|
|
37
|
+
id: record.id,
|
|
38
|
+
existed: true,
|
|
39
|
+
success: true
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('deletes integration with multiple entities', async () => {
|
|
44
|
+
const record = await integrationRepository.createIntegration(['e1', 'e2', 'e3'], 'user-1', { type: 'dummy' });
|
|
45
|
+
|
|
46
|
+
await useCase.execute(record.id, 'user-1');
|
|
47
|
+
|
|
48
|
+
const found = await integrationRepository.findIntegrationById(record.id);
|
|
49
|
+
expect(found).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('error cases', () => {
|
|
54
|
+
it('throws error when integration not found', async () => {
|
|
55
|
+
const nonExistentId = 'non-existent-id';
|
|
56
|
+
|
|
57
|
+
await expect(useCase.execute(nonExistentId, 'user-1'))
|
|
58
|
+
.rejects
|
|
59
|
+
.toThrow(`Integration with id of ${nonExistentId} does not exist`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws error when user does not own integration', async () => {
|
|
63
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
64
|
+
|
|
65
|
+
await expect(useCase.execute(record.id, 'different-user'))
|
|
66
|
+
.rejects
|
|
67
|
+
.toThrow(`Integration ${record.id} does not belong to User different-user`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('throws error when integration class not found', async () => {
|
|
71
|
+
const useCaseWithoutClasses = new DeleteIntegrationForUser({
|
|
72
|
+
integrationRepository,
|
|
73
|
+
integrationClasses: [],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
77
|
+
|
|
78
|
+
await expect(useCaseWithoutClasses.execute(record.id, 'user-1'))
|
|
79
|
+
.rejects
|
|
80
|
+
.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('tracks failed delete operation for non-existent integration', async () => {
|
|
84
|
+
const nonExistentId = 'non-existent-id';
|
|
85
|
+
integrationRepository.clearHistory();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await useCase.execute(nonExistentId, 'user-1');
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const history = integrationRepository.getOperationHistory();
|
|
91
|
+
const findOperation = history.find(op => op.operation === 'findById');
|
|
92
|
+
expect(findOperation).toEqual({
|
|
93
|
+
operation: 'findById',
|
|
94
|
+
id: nonExistentId,
|
|
95
|
+
found: false
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('edge cases', () => {
|
|
102
|
+
it('handles deletion of already deleted integration', async () => {
|
|
103
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
104
|
+
|
|
105
|
+
await useCase.execute(record.id, 'user-1');
|
|
106
|
+
|
|
107
|
+
await expect(useCase.execute(record.id, 'user-1'))
|
|
108
|
+
.rejects
|
|
109
|
+
.toThrow(`Integration with id of ${record.id} does not exist`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('handles integration with complex config during deletion', async () => {
|
|
113
|
+
const complexConfig = {
|
|
114
|
+
type: 'dummy',
|
|
115
|
+
settings: { nested: { deep: 'value' } },
|
|
116
|
+
credentials: { encrypted: true }
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', complexConfig);
|
|
120
|
+
|
|
121
|
+
await useCase.execute(record.id, 'user-1');
|
|
122
|
+
|
|
123
|
+
const found = await integrationRepository.findIntegrationById(record.id);
|
|
124
|
+
expect(found).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('handles null userId gracefully', async () => {
|
|
128
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
129
|
+
|
|
130
|
+
await expect(useCase.execute(record.id, null))
|
|
131
|
+
.rejects
|
|
132
|
+
.toThrow(`Integration ${record.id} does not belong to User null`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('handles undefined userId gracefully', async () => {
|
|
136
|
+
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy' });
|
|
137
|
+
|
|
138
|
+
await expect(useCase.execute(record.id, undefined))
|
|
139
|
+
.rejects
|
|
140
|
+
.toThrow(`Integration ${record.id} does not belong to User undefined`);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|